Skip to content
Ashish.
All posts
Diagram illustrating Keycloak's Quarkus runtime graph with SPI loaders, JPA providers, and theme engines.

Keycloak Architecture Deep Dive: Internal Components and Data Flow

An examination of Keycloak architecture, internal components, SPI extensions, themes, and the core data model for advanced developers.

By Ashish SrivastavaPart 1 of Keycloak Masterclass Series

Most developers treat Keycloak as a black box that accepts a username and password and returns a token. This view fails when debugging race conditions in custom authenticators or optimizing database performance under load. To understand Keycloak at an advanced level, you must discard the "application" mental model and adopt the "runtime graph" model. Keycloak is a Quarkus application where the entire lifecycle—from bootstrapping to request termination—is driven by a dependency injection container and a service loader pattern that resolves implementations dynamically.

This article is Part 1 of the Keycloak Masterclass Series.

The Bootstrapping Mechanism: Quarkus and Services

The process begins before a single HTTP packet is received. When the Keycloak server starts, the Quarkus framework initializes the KeycloakDeployment object. This is not a standard ServletContext; it is a specialized deployment descriptor that aggregates configuration from standalone.xml or environment variables.

Crucially, Quarkus uses a build-time and run-time split. Build-time processing scans the classpath for annotations like @Provider to generate bytecode optimizations. Run-time, it constructs the Services registry. This registry is the central nervous system. Every component, from the RealmProvider to the ThemeResourceProvider, is registered here.

Consider a scenario where the server starts. The KeycloakMain class triggers the KeycloakDeploymentFactory. This factory instantiates the SPI loader. The loader does not just find classes; it resolves the implementation of specific interfaces based on configuration. If you define a custom UserStorageProvider in your configuration, the SpiLoader locates the class, instantiates it, and binds it to the Services registry. No manual wiring occurs in the main thread; the container handles the graph construction.

Keycloak relies on @Provider (from org.keycloak.provider.Provider) and utilizes Jandex classpath scanning or explicit META-INF/services entries for extension discovery. The annotation @Extension is not a standard Keycloak annotation for this purpose.

The Core Data Model: Abstraction vs. Implementation

Once the services are bound, the application enters a state where it can resolve data requests. The most confusing aspect for advanced users is the distinction between the UserModel interface and the JpaUserProvider implementation.

Keycloak relies heavily on the Spi to abstract data storage. The core interfaces, located in the server-spi module, define contracts like UserModel, RoleModel, and GroupModel. These are purely in-memory abstractions. They do not know about SQL, LDAP, or NoSQL.

When an authentication request arrives, the AuthenticationManager invokes the UserModel methods (e.g., getGroups()). If the realm is configured with a JpaUserProvider, the call is delegated to the JpaUserProvider. This provider translates the interface method into a JPA query against the keycloak database.

Let's trace a specific artifact: the UserEntity. In the database, this is a row in the USER_ENTITY table. When UserModel is requested, the JpaUserProvider fetches the entity, wraps it in a UserAdapter, and returns the interface. This indirection is critical. It allows Keycloak to swap the underlying storage (e.g., to an LDAP adapter) without changing the authentication logic. The UserModel remains the constant contract.

However, this abstraction introduces a performance cost. Every access to getGroups() might trigger a database query unless the UserAdapter has cached the result in the SessionContext. Advanced tuning often involves configuring the CacheConfig to keep frequently accessed user attributes in the Infinispan cluster cache, bypassing the database entirely for subsequent reads.

The SPI Extension Point: Customizing Behavior

The Service Provider Interface (SPI) is the mechanism that allows Keycloak to be extensible without forking the codebase. Every major component—authentication flows, user storage, token stores, and event listeners—is an SPI.

To extend the authentication flow, you create a class annotated with @Provider and implement an interface like Authenticator. During the bootstrapping phase, the SpiLoader scans the classpath. It finds your class, checks if it matches the Authenticator interface, and registers it in the AuthenticatorFactory registry.

When a request hits the AuthenticationFlow, the FlowExecutor iterates through the configured steps. For each step, it queries the AuthenticatorFactory registry for the matching implementation. It then invokes the authenticate() method.

Consider a custom scenario: a TwoFactorAuthenticator. You implement the Authenticator interface. Your class is loaded by the SpiLoader. When the flow reaches the TwoFactorAuthenticator step, the FlowExecutor calls your authenticate() method. Inside this method, you access the ClientSession to store the OTP code. The ClientSession is a transient, in-memory structure that survives the HTTP request but is not persisted to the database until the flow completes successfully.

This separation of concerns means your custom code never touches the database directly unless you explicitly inject a RealmProvider. The SPI ensures that your logic runs within the same security context and transaction boundaries as the core.

Theme Rendering Pipeline: Resource Resolution

Keycloak themes are not just CSS files; they are a rendering engine driven by FreeMarker. The architecture here is a hierarchical resource resolution system.

When a user requests a login page, the request is routed to the ThemeResourceProvider. This provider does not serve static files directly. Instead, it resolves the theme path. The resolution order is strict: base -> realm -> default.

Imagine a realm named my-realm. If a file login.ftl exists in the my-realm/login directory, it overrides the default/login version. If it does not exist, the system falls back to the default version. The ThemeResourceProvider loads the FreeMarker template, injects the models (which contain the realm, user, and client data), and renders the HTML.

This mechanism is stateless regarding the file system but stateful regarding the configuration. The ThemeResourceProvider caches the resolved theme in memory. If you update a template file on disk, Keycloak will not see the change until the server is restarted or the cache is explicitly cleared via the admin API. This is a common source of confusion for developers who expect hot-reloading of themes.

The data flow here is: HTTP Request -> ThemeResourceProvider -> ThemeResolver -> FreeMarkerEngine -> Response. The models passed to FreeMarker are populated by the AuthenticationProcessor which has already validated the credentials.

Request Routing and Protocol Handlers

Finally, we must address how the HTTP request becomes an OIDC interaction. Keycloak uses a set of JAX-RS resources to handle protocol endpoints. The path /protocol/oidc is mapped to the AuthorizationEndpoint and TokenEndpoint resources.

When a request hits /protocol/oidc/authz, the AuthorizationEndpoint resource is invoked. This resource does not perform authentication itself. It delegates to the AuthenticationManager. The AuthenticationManager orchestrates the flow: it checks the SessionCookie, validates the state parameter to prevent CSRF, and triggers the Authenticator chain.

The critical artifact here is the ClientSession. This object holds the transient state of the OIDC exchange. It contains the client_id, redirect_uri, scope, and the authenticated_user. The ClientSession is stored in the Infinispan cache. If the client is configured for "offline sessions," the session is also persisted to the database.

When the authentication flow completes, the AuthorizationEndpoint constructs the AuthorizationResponse. It generates the authorization code, encrypts it, and redirects the user to the redirect_uri with the code in the query string. The TokenEndpoint then receives this code, validates it against the ClientSession in the cache, and issues the JWT.

This architecture ensures that the core authentication logic is decoupled from the protocol implementation. The AuthorizationEndpoint handles the OAuth2 spec, while the Authenticator handles the identity verification.

Common Pitfalls

Understanding the internals is vital to avoiding subtle bugs. Three common pitfalls arise from misinterpreting the data flow and caching mechanisms:

  1. Session Persistence Assumptions: Developers often assume ClientSession is always persisted. In reality, it resides in the Infinispan cache by default. If the cache evicts entries due to memory pressure or expiration policies, "offline" flows may fail unexpectedly unless explicitly configured to write to the database.
  2. Theme Caching Latency: Because the ThemeResourceProvider caches resolved themes in memory, updating a .ftl file on disk will not reflect immediately. Relying on hot-reloading for theme debugging leads to confusion; the server must be restarted or the cache cleared via the admin API to see changes.
  3. SPI Loader Ordering: The order in which the SpiLoader registers providers can impact behavior if multiple providers implement the same interface. While Keycloak generally selects the first match or uses specific priority markers, relying on implicit ordering without explicit configuration can lead to non-deterministic behavior in complex environments.

Practical Takeaways

To master Keycloak architecture, internalize these three mental models:

  • The Contract is King: Treat UserModel and Authenticator interfaces as immutable contracts. Your custom logic should focus on implementing these contracts, not manipulating the underlying storage directly.
  • State is Transient: Recognize that ClientSession is ephemeral. Design your flows assuming the state might be lost if the cache is cleared, and rely on the database only for explicit "offline" requirements.
  • Caching is Aggressive: Assume that lookups (themes, configurations, user attributes) are cached heavily. When debugging performance issues, verify cache hit rates and invalidation strategies before suspecting database bottlenecks.

FAQ

Q: Can I override the default JpaUserProvider without forking Keycloak? A: Yes. By implementing the UserStorageProvider interface and annotating it with @Provider, you can register a custom provider via the SPI. Ensure your provider has a lower priority or specific configuration to take precedence over the default.

Q: Why does my custom theme not update after editing the file? A: Keycloak caches theme resources in memory to improve performance. The ThemeResourceProvider does not watch the file system for changes. You must restart the server or use the Admin API to clear the theme cache.

Q: Is @Extension used for Keycloak SPI discovery? A: No. @Extension is not a standard Keycloak annotation. Keycloak relies on @Provider from the org.keycloak.provider package, combined with Jandex classpath scanning or META-INF/services entries to discover extensions.

Conclusion

Keycloak's power lies in its strict separation of concerns. The SPI loader decouples extensions from the core. The JPA provider decouples storage from the data model. The theme engine decouples presentation from logic. For an advanced developer, understanding these internal mechanisms allows you to debug complex authentication flows, optimize database performance by understanding caching layers, and safely extend functionality without breaking the core application. The "black box" is actually a highly structured graph of services, each with a defined contract and lifecycle.

Related posts