How can I restrict client access to only one group of users in keycloak? - single-sign-on

I have a client in keycloak for my awx(ansible tower) webpage.
I need only the users from one specific keycloak group to be able to log in through this client.
How can I forbid all other users(except from one particular group) from using this keycloak client?

I found a solution which does not require the scripts extension or any changes on the flow.
The key for this solution are the Client Scopes. An application which wants to to authorize a user needs a scope like email or uid, right? What if you only pass them to an application if a user is in a specific group?
In the following, my client application name is App1.
Solution:
Go to your client roles (realm -> Clients -> click App1 -> Roles)
Click 'Add Role' -> enter Name (e.g. 'access') -> click 'Save'
Go to Client Scopes (realm -> Client Scopes)
Click on the scope which is needed by your client application (e.g. 'email')
Assign Client Role 'access' in 'Scope' Tab by choosing client application 'App1' in Drop Down 'Client Roles'
Now, you won't be able to log into your client application App1 anymore, as the role 'access' is not assigned to any user or group. You can try.
Let's create a new group and assign the role and a user to it.
Create Group (realm -> Groups -> Click 'New' -> enter Name 'App1 Users' -> Click Save)
In the Group, choose 'Role Mappings', choose 'App1' in Client Roles drop down, and assign the role 'access'
Assign User to 'App1 Users' (realm -> Users -> Click on User -> Groups -> Select 'App1 Users -> Click Join)
Voila, the chosen user can log into App1.

On Keycloak admin console, go to Clients menu, select your client. On the client configuration page, set Authorization Enabled: On, click Save. A new Authorization tab should appear, go to it, then to the Policies tab underneath, click Create Policy and select Group-based policy. There, you can restrict access to specific groups, assuming you have defined your groups via the Groups menu already.
--EDIT 2019-11-08--
As mentioned in comments, Client Protocol must be set to openid-connect and Access Type must be set to confidential, in order to make the Authorization Enabled option visible.

Follow-up to Allan's answer: His approach is working (for me ;-) ), though I had some struggle on how to deploy it. This is how I did it:
Bundle script in a JAR file as documented here, deploy it by copying to standalone/deployments/ (see manual link)
Enable scripts: Start Keycloak with -Dkeycloak.profile.feature.scripts=enabled
In your realm, create a new flow. Duplicate the Browser flow in a required subflow, and add the script authenticator as final (required) element:
Now add to all clients which should be restricted a client role feature:authenticate. Users which don't bear that role won't get access to the application.

If it can help, here is a script which helps implementing this behaviour for any client: if the client contains a given role (here it is called feature:authenticate), then the script checks whether the user has the role and shows an error page (a new template that needs to be deployed in the theme) if not.
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
function authenticate(context) {
var MANDATORY_ROLE = 'feature:authenticate';
var username = user ? user.username : "anonymous";
var client = session.getContext().getClient();
LOG.debug("Checking access to authentication for client '" + client.getName() + "' through mandatory role '" + MANDATORY_ROLE + "' for user '" + username + "'");
var mandatoryRole = client.getRole(MANDATORY_ROLE);
if (mandatoryRole === null) {
LOG.debug("No mandatory role '" + MANDATORY_ROLE + "' for client '" + client.getName() + "'");
return context.success();
}
if (user.hasRole(mandatoryRole)) {
LOG.info("Successful authentication for user '" + username + "' with mandatory role '" + MANDATORY_ROLE + "' for client '" + client.getName() + "'");
return context.success();
}
LOG.info("Denied authentication for user '" + username + "' without mandatory role '" + MANDATORY_ROLE + "' for client '" + client.getName() + "'");
return denyAccess(context, mandatoryRole);
}
function denyAccess(context, mandatoryRole) {
var formBuilder = context.form();
var client = session.getContext().getClient();
var description = !mandatoryRole.getAttribute('deniedMessage').isEmpty() ? mandatoryRole.getAttribute('deniedMessage') : [''];
var form = formBuilder
.setAttribute('clientUrl', client.getRootUrl())
.setAttribute('clientName', client.getName())
.setAttribute('description', description[0])
.createForm('denied-auth.ftl');
return context.failure(AuthenticationFlowError.INVALID_USER, form);
}

I solved it like this:
Create a new role in Keycloak.
Assign this role to the group.
Create a new authentication script in Kycloak. Configure which role is allowed upon login (e.g. user.hasRole(realm.getRole("yourRoleName"))).
In the client's settings, under "Authentication Flow Overrides", choose the authentication script that was just created.

You can use this extension to restrict access to a specific group: https://github.com/thomasdarimont/keycloak-extension-playground/tree/master/auth-require-group-extension

according docu https://www.keycloak.org/docs/6.0/server_admin/#executions u have to active that feature to add some custom scripts with "add execution".
bin/standalone.sh|bat -Dkeycloak.profile.feature.scripts=enabled
#Allan solution with feature:authenticate looks good to me

I tried Allan's solution and it is working fine using Keycloak 11.0.3 but it has some cons mentioned below. Here is my solution for the authenticator script which does not grant access for users if they are not member at least one of the given groups. In such case a unique error message is shown.
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
function authenticate(context) {
var allowed_groups = ['foo', 'bar'];
var username = user ? user.username : "anonymous";
var groups = user.getGroups();
var group_array = groups.toArray();
for (var i in group_array) {
var gn = group_array[i].getName();
if (allowed_groups.indexOf(gn) >= 0) {
LOG.info("Access granted for user '" + username + "' for being member of LDAP group '" + gn + "'");
return context.success();
}
}
LOG.info("Access denied for user '" + username + ". for not being member of any of the following LDAP groups: " + allowed_groups);
context.failure(AuthenticationFlowError.IDENTITY_PROVIDER_DISABLED, context.form().setError(
"User doesn't have the required LDAP group membership to view this page", null).createForm("error.ftl"));
return;
}
There are two minor user experience related cons with this solution worth mentioning:
When a not logged in user tries to connect to a client which access gets denied by the authenticator script the whole authentication flow is considered failure. This means the user doesn't get logged in into Keycloak despite the fact they provided the correct credentials
When a logged in user tries to connect to a client which access gets denied by the authenticator script the Keycloak login page is displayed (without showing any error message) which is deceptive as the user can have the false feeling they are not logged in
In addition if you maintain multiple clients and you need to have different groups (or roles) checked per client then you have to implement as many new authentication flows as many different checks you need. In short the solution works, but it has some disadvantages. I believe a simple feature such as restricting the access based on groups or roles is essential for an identity and access management system and should be supported natively!

2021 year - Keycloak 7.4.1.GA
I solved it like this for SAML2:
Add new Authentication flow (Just copy existing one)
Add execution "Group Access Observer" and set it as Required
Actions -> Config on Group Access Observer line
Fill group name
Go to your client and change Authentication flow to created now.
Best Regards

With Keycloak >= 13.x you may want to try the "Allow/Deny Access" authenticators with conditions. You can assign a role to a group and build the condition based on the role.
If that is not flexible enough, try out this library that I have build to solve exactly that issue.

Related

Grafana sso using auth.generic_oauth

I have tried to implement sso in grafana using Oauth and ping id which is working as expected .
Access token provides the list of attributes, it shows all groups that i'm member of .so My workaround is to only members of the group mydomain_Monitoring_Portal can able to join sso using grafana
Used data-
{
"scope":[]
"client_id":"xxx-xxx-xxx-xxx-xxx",
" firstName": "myname",
"LastName":"lastname",
"emailAddress":"abc#gmail.com",
:memberOf":[
"CN=mydomain_Monitoring_Portal,OU=xyz,OU=SecurityGroup,DC=fiat,DC=com"
"CN=Monitoring,OU=abc,OU=Secret,DC=fiat,DC=com"
"CN=service,OU=def,OU=mount,DC=fiat,DC=com
],
"userType":"Employee",
"userId":"nb656",
"username":"n656",
"exp":167895258
},
scope : openid email profile
i had tried to implement group_attribute_path in grafana.ini file no luck. all members of the other group also able to join
groups_attribute_path = memberOf[?contains(#, 'mydomain_Monitoring_Portal') == `true`]
expecting is Only employees which is in group mydomain_Monitoring_Portal can able to join garfana using sso
Doc is your friend: https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/generic-oauth/#role-mapping
You need to configure role mapping role_attribute_path + role_attribute_strict = true which denies user access if no role or an invalid role is returned.

Need to display JWT name under the Grant Types tab in WSo2 application Manager(WSo2 API store)

Working on WSo2 Api manager(api store), here i need to display the JWT name under Grant types tab, please see the attached screen shot for more information. Can some body suggest , what i need to do ,for displaying the JWT name under the Grant Types tab.
Can add the name by:
Open /repository/deployment/server/jaggeryapps/store/site/themes/wso2/js/applications.js
Add a new entry under the "config" of the "GrantType" object (first variable) with "urn:ietf:params:oauth:grant-type:jwt-bearer" as the key and "JWT" (Or any desired name) as the value.
eg:
var GrantTypes = function (available) {
this.config = {
...
...
"urn:ietf:params:oauth:grant-type:jwt-bearer":"JWT"
}
...
...
};
You can find these steps in [1].
[1] https://github.com/wso2/product-apim/issues/2433

Demandware OCAPI modify order

I've built a tiny program that helps Identify orders in Demandware that have incorrect status, e.g: (status: new, open, completed and shipping-status: not-shipped, shipped).
I basically just use order_search from OCAPI and compare the results with our ERP.
However now I want to automate some of the fixing of status, which would require me to use the /orders/{order_no} GET and PATCH calls, however when I do so, I get the following message:
{ type: 'AccessWithoutUserForbiddenException',
message: 'An authenticated user is required in order to access resource.' }
According to the docs OAUTH for order_search uses: "Authentication via OAuth token.", however orders/{order_no} uses: "Authentication via OAuth token. A valid user is required."
So what would be the right strategy for becoming a valid user?
a valid user for getting oAuth tokens is a Business Manager user. So please login to Business Manager and create a new user for your use cases and grant the necessary permissions.
After that you are able to execute the particular resources.
Christian
If you are using account.demandware.com as the host then it will throw below error
{ error: 'unauthorized_client', error_description: 'Client id
\'xxxxxxxxxxxxxxxxxxx\' has invalid credentials to use grant type
\'urn:demandware:params:oauth:grant-type:client-id:dwsid:dwsecuretoken\'.'
}
Instead you can change the host to your sandbox host. And try once again. It should work. I was also facing the same issue.
const key = new Buffer('business_manager_email_id' + ":" + 'business_manager_pwd' + ":" + 'client_pwd').toString("base64");
const options = {
url: 'https://<sandbox_host>/dw/oauth2/access_token?client_id=your_client_id',
method: "POST",
headers: {
'Authorization': "Basic " + key,
"Content-Type": "application/x-www-form-urlencoded",
},
body: "grant_type=urn:demandware:params:oauth:grant-type:client-id:dwsid:dwsecuretoken"
};

Spring remember-me with MongoDB does not delete tokens

I followed this tutorial to implement remember-me functionality with MongoDB.
The tokens are saved in the database when i click the rememberme checkbox in the login page. If i delete the db entry manually and the cookie JSESSIONID maxage has expired i am getting logged out and if the JSESSIONID has expired and the remember-me cookie does not, i am still logged in which is great.
All works well but i have a question. The removeUserTokens function is never called, should i manually delete the token entry from the database? If yes where should i implement this?
Thank you.
It has to be deleted manually (e.g. by batch process) per Java doc.
PersistentTokenBasedRememberMeServices
Note that while this class will use the date a token was created to
check whether a presented cookie is older than the configured
tokenValiditySeconds property and deny authentication in this case, it
will not delete these tokens from storage. A suitable batch process
should be run periodically to remove expired tokens from the database.
The abstraction (PersistentTokenRepository) used by PersistentTokenBasedRememberMeServices to store the persistent login tokens for a user.
After searching it a bit more i found that when i logout and having this to my configuration:
http.authorizeRequests().antMatchers("/signup", "/about").permitAll().antMatchers("/doctor/**")
.hasRole("DOCTOR").anyRequest().authenticated().and().rememberMe().rememberMeParameter("remember-me")
.tokenRepository(tokenRepository).tokenValiditySeconds(1209600).and().formLogin().loginPage("/login")
.failureUrl("/login?error=true").permitAll().and().logout().logoutUrl("/logout")
.deleteCookies("JSESSIONID").invalidateHttpSession(true).logoutSuccessUrl("/login").permitAll();....
the removeUserTokens method is called and the associated token is deleted from the db. I think the trick is made by:
.logout().logoutUrl("/logout")
.deleteCookies("JSESSIONID").invalidateHttpSession(true)
Also as notionquest said above i added a Spring cron job to run every Friday at 3 AM in case of something is left in the db.
#Scheduled(cron = "0 0 3 * * FRI")
public void doScheduledWork() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.WEEK_OF_MONTH, -2);
tokenRepository.deleteBeforeDated(calendar.getTime());
logger.info("INFO", "Cron job runed at " + new Date() + " until " + calendar.getTime() + " !");
}

Programmatically get user identity from Azure ACS

This question is a bit noobie, but i can't find the information over the internet (perhaps i'm search wrongly?)
We have an Azure ACS configured and we using it as auth service for our website.
But now we need to build an application, which, by known username and password, will receive users claims from ACS. Is this possible?
Yes, it's possible.
One thing to note - Using ACS, you can choose a variety of different token providers to allow (aka STS-es). Each of those provide a different set of claims to you as a default, so you might need to enrich these.
Here's a snippet of code that you can try to see what claims are coming back from ACS in your code already:
// NOTE: This code makes the assumption that you have .NET 4.5 on the machine. It relies on
// the new System.Security.Claims.ClaimsPrincipal and System.Security.Claims.ClaimsIdentity
// classes.
// Cast the Thread.CurrentPrincipal and Identity
System.Security.Claims.ClaimsPrincipal icp = Thread.CurrentPrincipal as System.Security.Claims.ClaimsPrincipal;
System.Security.Claims.ClaimsIdentity claimsIdentity = icp.Identity as System.Security.Claims.ClaimsIdentity;
// Access claims
foreach (System.Security.Claims.Claim claim in claimsIdentity.Claims)
{
Response.Write("Type : " + claim.Type + "- Value: " + claim.Value + "<br/>");
}
Adam Hoffman
Windows Azure Blog - http://stratospher.es
Twitter - http://twitter.com/stratospher_es