j.a./ writings / understanding-keycloak-spis

Understanding Keycloak Service Provider Interfaces

March 1, 2025 · 5 min read

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 factor
  • LoginProtocol: control how an auth protocol behaves at a low level; most teams never need this
  • UserStorage: connect Keycloak to an external user store, like a legacy database or custom LDAP schema
  • UserProfile: define validation rules and read/write constraints for user attributes
  • IdentityProvider: add support for a social or enterprise IdP that Keycloak does not know about
  • IdentityProviderMapper: control how claims from an external IdP map to Keycloak user attributes or claims
  • ProtocolMapper: add custom claims to access tokens, ID tokens, or the userinfo response
  • EventListener: run code on login, logout, token issuance, errors, and admin actions
  • EventStore: persist events to your own storage backend
  • FormAction: extend the registration or profile update forms with extra fields or validation
  • EmailSender: swap in your own email provider
  • ThemeSelector: pick a login theme dynamically per realm or client
  • ScheduledTask: run background jobs on a schedule
  • TokenIntrospector: 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/services file 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=DEBUG during 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 EventListener SPI runs synchronously for some event types. If you put a slow external call inside onEvent(), 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.