An authenticator to check your Keycloak version
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 :
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.
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 authenticatorvoid action(AuthenticationFlowContext context);
if we returned a form to the user in the previous step, the form submission will be catched hereboolean 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 authenticatorvoid setRequiredActions(KeycloakSession session, RealmModel realmModel, UserModel user);
set required actions for this authenticatorList<RequiredActionFactory> getRequiredActions(KeycloakSession session)
get all needed required actions for this authenticatorboolean 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.
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.
An extension of an “authenticator” is a “conditional authenticator”.
It adds a new method :
boolean matchCondition(AuthenticationFlowContext context);
This kind of authenticator is used for conditional subflows.
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
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”.
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();
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” 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”.
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.
By default, the authentication flow “browser” is not editable. Duplicate it, add a new step in “browser form” flow :
Then our newly created authenticator :
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 !
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 :
Put your own breakpoints, manipulate your authentication flow and you will be able to execute your authenticator step by step.
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.