Restricting login to some clients based on client roles

I need to restrict access to some clients in keycloak based on roles. So, if a user wants to login to the restricted client A, the user needs to have the client role A.
If the user wants to login to client B, which is not a restricted client, the user should be able to login.

I don’t want to use Authorization. So, my current solution looks like this:

  • If a user should be allowed to login to a restricted client, the user has to have the specific client role (assuming that a restricted client has such a client role).
  • I implemented a custom authentication step in Java code. This code checks if the user has the role for the restricted client, see my Authenticator code at the end of the question.
  • I added the custom step to the browser flow:

Result:
The user cannot login to the restricted client if the user does not have the required role - IF the user does not have a current session.

Problem:
If the user has already successfully logged in to client B (which is not restricted), the user is automatically logged in to client A as well - because of the cookie authentication. So, my custom authentication step is not executed if cookie authentication was already successful.

I could find an explanation to this behavior in the keycloak documentation, but no solution: “Since the cookie provider returned success and each execution at this level of the flow is alternative, Keycloak does not perform any other exections.”

Does anyone have a solution to this?

Java Custom Authentication Step:

public class ClientRoleAuthenticator implements Authenticator {

private static final Logger logger = Logger.getLogger(ClientRoleAuthenticator.class);

@Override
public void authenticate(AuthenticationFlowContext context) {
    AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
    ClientModel client = authenticationSession.getClient();

    if (client == null) {
        logger.error("Client is null.");
        context.failure(AuthenticationFlowError.INTERNAL_ERROR);
        return;
    }

    String clientId = client.getClientId();
    logger.info("clientId: " + clientId);
    
    RoleModel role = client.getRole(clientId);

    if (role == null || !clientHasSameNameRole(client, role)) {
        // Skip authentication step if client has no role of the same name
        logger.info("Client has no role of the same name. Continuing...");
        context.success();
        return;
    }

    UserModel user = authenticationSession.getAuthenticatedUser();
    if (user == null) {
        // Skip authentication step if no user context
        logger.info("No user context. Continuing...");
        context.success();
        return;
    }

    if (user.hasRole(role)) {
        // Continue authentication flow if user has the same name role
        logger.info("User has the same name role. Continuing...");
        context.setUser(user);
        context.success();
    } else {
        // Intercept the authentication flow if user does not have the same name role
        logger.error("User does not have the same name role. Access denied.");
        context.failure(AuthenticationFlowError.ACCESS_DENIED);
    }
}

private boolean clientHasSameNameRole(ClientModel client, RoleModel role) {
    return role != null && role.getName().equals(client.getClientId());
}

@Override
public void action(AuthenticationFlowContext authenticationFlowContext) {
}

@Override
public boolean requiresUser() {
    return false;
}

@Override
public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
    return false;
}

@Override
public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {

}

@Override
public void close() {

}

}

Take a look at this extension, which was designed to solve the problem you are describing: GitHub - sventorben/keycloak-restrict-client-auth: A Keycloak authenticator to restrict authorization on clients

3 Likes

Seems to be exactly what I need!

Thank you for that!

1 Like

Nevertheless, I don’t recommend enforcing it in the IdP because you are mixing authentication and authorization. Eventually, the authorization rules or the UX that you want to achieve will change, and you will end up maintaining all of them in the IdP. The clean solution is to delegate authentication to the IdP, and the authorization is the responsibility of each app or gateway acting as a PEP.

3 Likes