On 20/08/2019 12:29, Chris Boot wrote:
Hi all,
I'm trying to restrict which OIDC clients users can login to based on
roles or group membership. I can't believe this isn't something
built-into Keycloak yet, but it seems that way.
[snip]
As far as I can tell, the only way to make this work is using a
custom
authentication flow:
https://stackoverflow.com/a/54384513/9531301
Hi all,
Because this question seems to have been asked over and over and
precious few answers have appeared, and I've spent all day hacking at
this, here is what I've come up with. I'm running Keycloak 6.0.1 for
anyone who stumbles across this post in search results in the future.
First, enable the "scripts" profile in Keycloak. Basically pass
"-Dkeycloak.profile.feature.scripts=enabled" on the command-line or
whatever variant of that which makes sense for your environment.
Then, in your relevant realm, copy the Browser and/or Direct Grant
flows, and call it something like "Browser + Required Roles" or whatever.
In your new flow, press "Add execution" and add a Script execution.
Under the Actions menu for the new Script, choose Config. I configured
mine as:
Alias: require-client-roles
Script Name: require-client-roles
Description: Check that a user has all required roles assigned for a client
Paste in the script below (also available at
http://paste.debian.net/1096714/).
Once saved, go back to your flow and change the Requirement to REQUIRED.
Now you need to configure your client(s):
- Pick the relevant client in Clients.
- In the Settings tab, expand "Authentication Flow Overrides" and choose
your new custom flow(s).
- In the Roles tab, create a new role.
- The name and description don't matter.
- Once created, go into Attributes and set a new attribute "required" to
"true". This is what the script looks for.
At this point, nobody should be able to login to the client anymore.
Users should be able to login again once the new role is assigned to them.
Is there a way of stopping such clients from being shown on the
Account
Management => Applications screen without globally removing the
offline_access role for all users?
I'd still like answers for this one. Basically pretending that "Offline
access" isn't there should do the trick, but how? A custom theme?
I hope this helps!
Chris
The script:
/*
* Check that a user has all required roles assigned for a client.
*
* This implements per-client authentication for Keycloak. Roles added to a
* client can have the "required" attribute set to "true", which will
mark them
* as being required. Any users who do not have all the required roles
will be
* unable to authenticate using this script.
*
* Copyright (c) 2019 Tiger Computing Ltd.
* Author: Chris Boot <crb(a)tiger-computing.co.uk>
*/
// import enum for error lookup
AuthenticationFlowError =
Java.type("org.keycloak.authentication.AuthenticationFlowError");
function authenticate(context) {
var username = user ? user.username : "anonymous";
LOG.info(script.name + ": trace auth for: " + username);
var client = session.getContext().getClient();
LOG.info(script.name + ": auth to client: " + client.getClientId());
if (user) {
// Find all client roles marked as required
var requiredRoles = Java.from(client.getRoles())
.filter(function(role) {
var required = role.getFirstAttribute("required");
return (required && required != "false");
});
// Play it safe: check that we have found at least one required role
if (requiredRoles.length === 0) {
LOG.warn(script.name + ": no required roles found, failing
safe");
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
// Check that the user has all required roles
if (requiredRoles.every(function(role) user.hasRole(role))) {
LOG.info(script.name + ": user has all required roles");
context.success();
return;
}
// The user is missing one or more required roles
var missingRoles = requiredRoles
.filter(function(role) !user.hasRole(role))
.map(function(role) role.getName());
LOG.warn(script.name + ": missing role(s): " + missingRoles);
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
LOG.warn(script.name + ": denying anonymous authentication");
context.failure(AuthenticationFlowError.INVALID_USER);
}
--
Chris Boot
bootc(a)boo.tc