Keycloak is flexible by design. At the core of that flexibility is its Service Provider Interface (SPI) system, a plugin architecture that lets you replace or extend almost any internal behaviour. While working in my current role I have had multiple instances where we needed a custom way of either doing authentication or writing special properties into claims using Keycloak mappers. In this article I will explain the main idea behind SPIs and give a quick introduction to help you get started with customising your Keycloak setup.
What is an SPI?
An SPI in Keycloak is a contract (a Java interface) that defines a capability. Keycloak ships with default implementations, but you can swap them out with your own by packaging them as a JAR and dropping them into the providers/ directory.
A Map of the SPI Landscape
Keycloak has a lot of SPI types. The one you eventually choose depends on your use case:
Authenticator: add a custom step to the login flow, such as CAPTCHA or a bespoke second factorLoginProtocol: control how an auth protocol behaves at a low level; most teams never need thisUserStorage: connect Keycloak to an external user store, like a legacy database or custom LDAP schemaUserProfile: define validation rules and read/write constraints for user attributesIdentityProvider: add support for a social or enterprise IdP that Keycloak does not know aboutIdentityProviderMapper: control how claims from an external IdP map to Keycloak user attributes or claimsProtocolMapper: add custom claims to access tokens, ID tokens, or the userinfo responseEventListener: run code on login, logout, token issuance, errors, and admin actionsEventStore: persist events to your own storage backendFormAction: extend the registration or profile update forms with extra fields or validationEmailSender: swap in your own email providerThemeSelector: pick a login theme dynamically per realm or clientScheduledTask: run background jobs on a scheduleTokenIntrospector: customise token introspection for resource servers with non-standard validation needs
Building a Custom SPI
Every SPI has two parts: the Provider (the actual logic) and the ProviderFactory (responsible for instantiation and configuration).
public class MyEventListenerProvider implements EventListenerProvider {
@Override
public void onEvent(Event event) {
if (event.getType() == EventType.LOGIN) {
// do something on login
}
}
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {}
@Override
public void close() {}
}
A Closer Look: The Authenticator SPI
Since custom authentication flows come up so often, it is worth walking through the Authenticator SPI in a bit more detail.
The interface has two main methods. authenticate() is called when the flow first reaches your step. This is where you decide whether the user can proceed or whether you need to challenge them. action() is called when the user submits a form in response to that challenge.
Here is a stripped-down example of a CAPTCHA authenticator:
public class MyCaptchaAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
Response challenge = context.form().createForm("captcha.ftl");
context.challenge(challenge);
}
@Override
public void action(AuthenticationFlowContext context) {
String token = context.getHttpRequest()
.getDecodedFormParameters()
.getFirst("captcha-token");
if (isValid(token)) {
context.success();
} else {
context.failureChallenge(
AuthenticationFlowError.INVALID_CREDENTIALS,
context.form().createForm("captcha.ftl")
);
}
}
@Override
public boolean requiresUser() { return false; }
@Override
public void close() {}
}
The factory pairs with it and tells Keycloak how to create an instance and what metadata to display in the Admin Console:
public class MyCaptchaAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "my-captcha";
@Override
public String getId() { return PROVIDER_ID; }
@Override
public Authenticator create(KeycloakSession session) {
return new MyCaptchaAuthenticator();
}
}
To wire it into a login flow, go to the Admin Console under Authentication, create or copy an existing flow, and add your authenticator as a step.
Registering the Provider
Keycloak uses Java's ServiceLoader mechanism. Create the file:
META-INF/services/org.keycloak.events.EventListenerProviderFactory
And put your factory class name inside it. Package everything as a JAR, drop it into providers/, and run kc.sh build.
Common Mistakes
A few things trip people up when working with Keycloak SPIs for the first time.
-
Missing service file. If you forget to create the
META-INF/servicesfile with your factory class name, Keycloak will not load your SPI at all. It fails silently, which can waste a lot of debugging time. Always double-check that the file exists and that it contains the correct fully qualified class name. -
Version mismatch. SPI interfaces change between Keycloak versions. If you compiled your provider against Keycloak 21 and then deploy it to Keycloak 24, there is a good chance a method signature has changed. Pin your Keycloak dependency in your build file and keep it in sync with the version running in production.
-
Debugging is harder than you expect. Keycloak's default logging level will not tell you much when something goes wrong inside your provider. Run the server with
--log-level=DEBUGduring development, and be generous with exception logging inside your code. -
Custom auth code can hurt security. When you implement an Authenticator or a custom ProtocolMapper, you are writing security-critical code. Make sure you understand the flow context before shipping. It is easy to accidentally skip a check or expose something in a token that should not be there.
-
Slow event listeners affect the login path. The
EventListenerSPI runs synchronously for some event types. If you put a slow external call insideonEvent(), it adds latency directly to the login experience. Keep event listeners lightweight and push any heavy work to an async queue.
SPIs give you a clean seam to extend Keycloak without forking it. The pattern is always the same: implement the interface, write a factory, register via ServiceLoader. Once you understand this, any Keycloak customisation becomes approachable.