Extending Keycloak—required actions: user must be approved

After some insightful weeks of diving into the Ory platform, I am reverting back to Keycloak to investigate some other of its interesting features. The last few weeks spent in the Ory-land were enlightening.

One of my previous post, Introduction to Keycloak Authorization Services[1], gets about 20 daily reads but authorization services isn’t the only awesome thing about Keycloak.

§service provider interfaces

Keycloak’s extensibility is what absolutely blows my mind. Keycloak defines a number of service provider interfaces (SPI)[2] which allow the developer to tap into and add completely new functionality. Authorization is a bit like CRM or ERP, every organization has their own quirks.

SPI implementations are written in Java—any language targeting the JVM, really—and deployed to the Keycloak instance by placing them in a predefined directory.

The SPI deployment supports hot reloading, SPIs can be updated while the server is running.

§testing the waters

I’m going to start this little series by looking at a required action provider. A required action provider hooks into the registration and login process and executes an action which allows amending the flow and conditionally allow or reject the login attempt.

The question: how difficult would it be to implement the following scenario:

  • The user registers via self service.
  • The user is not allowed to log in via self service until a specific attribute is set; this attribute would be set by an administrative operator; effectively requires a registration approval.
  • If the user account does not have the required attribute, the user is forwarded to an information page outside of Keycloak.

I’m not implementing the actual approval process because this is way out of scope of this article.

§the Java code

Any Keycloak provider requires, at minimum, two implementation classes:

  • A provider factory: a class implementing a specific SPI factory interface.
  • An implementation of the SPI initialized by the previously written factory.

The required action provider is one of the simplest SPIs available, but it does allow for some pretty wild implementations. For example, the out of the box WebAuthn support is one of those.

Here, the User must be approved factory, is pretty straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.gruchalski.idp.spi.actions;

import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

public class UserMustBeApprovedActionFactory implements RequiredActionFactory {

    private static final UserMustBeApprovedAction SINGLETON = new UserMustBeApprovedAction();

    @Override
    public RequiredActionProvider create(KeycloakSession session) {
        return SINGLETON;
    }

    @Override
    public void init(Config.Scope scope) {}

    @Override
    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {}

    @Override
    public void close() {}

    @Override
    public String getId() {
        return UserMustBeApprovedAction.PROVIDER_ID;
    }

    @Override
    public String getDisplayText() {
        return "User must be approved";
    }
}

This class implements the org.keycloak.authentication.RequiredActionFactory and creates a static action instance. The create, init and postInit methods are pretty neat because they allow us to configure the action based on whatever the Keycloak status is. They enable full integration with Keycloak runtime. This pattern exists across all of the Keycloak SPIs.

The actual action looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.gruchalski.idp.spi.actions;

import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;

public class UserMustBeApprovedAction implements RequiredActionProvider {

    public static String PROVIDER_ID = "USER_MUST_BE_APPROVED";

    @Override
    public void evaluateTriggers(RequiredActionContext requiredActionContext) {}

    @Override
    public void requiredActionChallenge(RequiredActionContext requiredActionContext) {
        if (requiredActionContext
                .getUser()
                .getAttributes()
                .containsKey("x-approved")) {
            requiredActionContext.success();
        } else {
            requiredActionContext
                .getAuthenticationSession()
                .setRedirectUri("https://account.gruchalski.com/errors/approval-required/");
            requiredActionContext.failure();
        }
    }

    @Override
    public void processAction(RequiredActionContext requiredActionContext) {}

    @Override
    public void close() {}
}

The requiredActionChallenge(RequiredActionContext) method is where the action happens. This method is called when the user enters the User must be approved login step; usually after submitting the login form. The requiredActionContext argument provides a number of interesting methods. For example, we can create forms with arbitrary fields asking the user for additional input. If we did that, we could process that input in the processAction(RequiredActionContext) method.

This action, however, is a binary decision - the user either has or does not have the attribute assigned. We can inspect the logging in user by looking up various details using the requiredActionContext.getUser() method. Here, the program checks whether the user account has the x-approved. If yes, the context is approved using the success() method. Otherwise, the request is redirected to an arbitrary URI which could provide detailed explanation of the reason.

To have this action available in Keycloak, a third file is required. The file is called org.keycloak.authentication.RequiredActionFactory and must be placed in resources/META-INF/services directory of the Java project. The content is simply:

com.gruchalski.idp.spi.actions.UserMustBeApprovedActionFactory

§deployment

Regardless on your Java packaging tool of choice, the outcome is a jar file containing the compiled classes and the resources directory. The jar file must be copied to the Keycloak /opt/jboss/keycloak/standalone/deployments directory. Give Keycloak a couple of seconds to load the classes, the status will be printed in the logs.

§enabling and testing

Sign in to Keycloak and navigate to the realm Authentication (left menu) / Required Actions tab. Click the Register button in top left table corner and select the User must be approved action from the list. Click Ok. The action will be added to the list.

If you want to enforce this action for every user in the realm, tick the Default action checkbox. Be careful, if you do this, make sure your current user you are logged in as, has the x-approved attribute assigned - otherwise you will lock yourself out! If in doubt, test on a temporary realm or on a Keycloak instance you can easily wipe.

The action can be enforced for individual users only. To do so, go to realm Users, find the account for which the action should be enforced and select it in the Required User Actions. Save the changes.

That’s it. Now, when the user tries to sign in and there is no required attribute, they will be redirected to the information page. This action also takes effect when using the direct password grant. Without the attribute, the response is:

1
2
3
4
{
    "error": "invalid_grant",
    "error_description": "Account is not fully set up"
}

In one of the future posts, I am going to look at adding custom forms to the action.