In this article we will explain through an example what is and how to use Keycloak Authenticator.

In Keycloak, an « authenticator » is a step in an authentication process, what we call « Authentication flow ».

An impressive list of authenticators are available with Keycloak out of the box :

Keycloak Authenticator explained

this list is available under « realm info » then « Provider info »

In this post, we will explore what an authenticator is, how it works and create one that checks if Keycloak is-up-to date or not.

Authenticator structure

An Authenticator is defined in the interface « Authenticator » : https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/authentication/Authenticator.java

7 methods :

  • void authenticate(AuthenticationFlowContext context); initial « entry point » for the authenticator
  • void action(AuthenticationFlowContext context); if we returned a form to the user in the previous step, the form submission will be catched here
  • boolean requiresUser(); Does this authenticator need a user context ? Depending on « when » we execute this authenticator, we can have a user already loaded by a previously executed authenticator. We will get the user with context.getUser()?
  • boolean configuredFor(KeycloakSession session, RealmModel realmModel, UserModel user); checks if the user is concerned or not by this authenticator
  • void setRequiredActions(KeycloakSession session, RealmModel realmModel, UserModel user); set required actions for this authenticator
  • List<RequiredActionFactory> getRequiredActions(KeycloakSession session) get all needed required actions for this authenticator
  • boolean areRequiredActionsEnabled(KeycloakSession session, RealmModel realm) Checks if all required actions are configured in the realm and are enabled

« authenticate » and « action » are the most important part of our authenticator. It defines the interaction between the user and our authentication method.

success, failure or challenge ?

Interactions between the authenticator and the user are done by using the « context » object.

  • success : the authentication is successful, ONLY FOR THIS AUTHENTICATOR. The authentication flow continues
  • failure : the authentication failed, if the authenticator is « alternative » the authentication flow continues. If authenticator is « required », authentication flow stops.
  • challenge : a challenge is required, with a form for example. This form is returned to the user. Form submission is retrieved in the « action » method.
Keycloak Authenticator explained

Authenticator and Conditional Authenticator

An extension of an « authenticator » is a « conditional authenticator ».

https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalAuthenticator.java

It adds a new method :

boolean matchCondition(AuthenticationFlowContext context);

This kind of authenticator is used for conditional subflows.

Keycloak Authenticator explained

For example, this subflow « Conditional OTP » defined as « conditional » checks the first authenticator, this one is called « user configured ».

If « user configured » returns « true », then the rest of the flow is executed. In this case, « OTP Form ».

Curious about the « OTP Form » authenticator ? Check it here :

https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java

Build our : check if Keycloak is up to date

Sources available here : https://github.com/please-openit/keycloak-check-version-authenticator

First of all, a standard Java project (here we use Maven), with dependencies :

  • org.keycloak.keycloak-server-spi
  • org.keycloak.keycloak-server-spi-private
  • org.keycloak.keycloak-services
  • org.keycloak.keycloak-core
  • org.jboss.logging.jboss-logging
  • com.google.code.gson.gson

Like any extension we make for Keycloak, we need a « Factory » (AuthenticatorFactory in our case) and we have to declare our factory in a resource file called « org.keycloak.authentication.AuthenticatorFactory ».

Get last release from Github

Github has a public API for the latest release. Keycloak has a release number with major.minor.minor.

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.github.com/repos/keycloak/keycloak/releases/latest"))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .GET()
        .build();

HttpResponse<String> response = null;
try {
    response = client.send(request, HttpResponse.BodyHandlers.ofString());
}
// catch ...
final Gson gson = new Gson();
final JsonObject jsonObject = gson.fromJson(response.body(), JsonObject.class);
return jsonObject.get("name").getAsString();

Get the current release, compare

When we have the version as string, remove the dots and compare those two numbers.

String sourceVersion = SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp()).getVersion();
String githubReleaseVersion = getLastReleaseFromGithub();

int versionAsInt = Integer.parseInt(sourceVersion.replace(".", ""));
int githubReleaseVersionAsInt = Integer.parseInt(githubReleaseVersion.replace(".", ""));

if(versionAsInt == githubReleaseVersionAsInt){
    authenticationFlowContext.success();
    return;
}

If the version is not up to date, we provide a challenge to the context. This challenge loads the « version.ftl » form we defined in our project (theme-resources/templates), with a set of attributes (« current » and « available »).

LoginFormsProvider form = authenticationFlowContext.form().setExecution(authenticationFlowContext.getExecution().getId());
form.setAttribute("current", sourceVersion);
form.setAttribute("available", githubReleaseVersion);
Response response = form.createForm("version.ftl");
authenticationFlowContext.challenge(response);

Challenge ?

« challenge » shows a form. This form has only an « ok » button (we can consider this form as an « information form »).

When we enter into the « action » method, we do nothing, just « success ».

Deploy

Just compile it with :

mvn clean install

Then, copy the generated JAR file into the « providers » directory.

We made a docker-compose.yml file for you.

Use it !

By default, the authentication flow « browser » is not editable. Duplicate it, add a new step in « browser form » flow :

Keycloak Authenticator explained

Then our newly created authenticator :

Keycloak Authenticator explained

Do not forget to make it « required ».

Define this browser flow as default (not recommended), or default flow for the « security admin console » client (in « advanced », then « authentication flow overrides »).

And that’s it !

Keycloak Authenticator explained

Debug

In docker-compose file, we added 2 environment variables :

DEBUG: "true"
DEBUG_PORT: '*:8787'

And a mapping with the host.

In intellij (or any Java environment), use a « remote JVM debug » on port 8787 :

Keycloak Authenticator explained

Put your own breakpoints, manipulate your authentication flow and you will be able to execute your authenticator step by step.

Conclusion

A great example of a specific use of an authenticator that is not an authentication method.

Never forget to check your software version, this authenticator is not a good way.

We added version check in the « Keycloak config checker » plugin : https://github.com/please-openit/keycloak-config-checker. Combined with an alerting job, an automatic check could be done.

Mathieu PASSENAUD
Les derniers articles par Mathieu PASSENAUD (tout voir)