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 :
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 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.
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.
Authenticator and Conditional Authenticator
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 :
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 :
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 !
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 :
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.
- How to enrich native metrics in KeyCloak - 21 août 2024
- Keycloak Authenticator explained - 7 mars 2024
- Keycloak OIDC authentication with N8N workflow - 1 décembre 2023