
Keycloak Architecture: Realms, Clients, and the Data Model
An examination of Keycloak architecture, focusing on realms, clients, and the underlying data model for platform and identity engineers.
Identity management systems fail when engineers treat them as black boxes. To build resilient infrastructure, you must understand the mechanism-level relationships between Realms, Clients, and the underlying data model. This guide explains how identity state is partitioned, trust is anchored, and data is persisted.
The Realm: A Logical Database Partition
The realm is the primary unit of isolation in Keycloak architecture. It is not merely a folder for settings; it is a distinct logical database containing all identity data. When you create a realm, you are creating a closed system. Users, groups, credentials, and roles created within production-realm have zero visibility to staging-realm.
This isolation is enforced at the query level. When a request arrives, Keycloak identifies the realm based on the context (often derived from the URL path or the client's registered redirect URIs). The persistence layer then scopes all SQL queries to that specific realm’s identifier. This means you cannot accidentally join a user from one tenant with roles from another unless you explicitly implement cross-realm federation.
Consider a platform engineer managing multiple customer tenants. Each tenant gets their own Keycloak realm. The architecture ensures that even if the underlying database is shared (e.g., PostgreSQL), the logical separation prevents data leakage. The realm also holds the cryptographic keys (RSA/ECDSA) used to sign JWTs. If you rotate keys in one realm, it does not affect the signature validation of another. This is critical for security: key compromise is contained within a single realm boundary. For more on this isolation, see the Keycloak Realm Documentation.
Clients: Defining Trust Boundaries
Clients in Keycloak define the contract for how external systems interact with the identity provider. A Client in Keycloak is not just an application name; it is a configuration object that defines how an external system interacts with the identity provider. The most common confusion arises around the distinction between public and confidential clients, which dictates the security guarantees of the OAuth 2.0 / OIDC flow.
A confidential client is an entity that can securely store a secret. Typically, this is a backend service or a web application running on a server you control. When this client initiates an Authorization Code Flow, it presents a client_secret to the Token Endpoint. Keycloak verifies this secret before issuing an access token. This creates mutual trust where the client authenticates itself to the server, distinct from transport-layer mTLS.
A public client, by contrast, cannot securely store a secret. These are typically Single Page Applications (SPAs) or mobile apps running in a browser or on a user’s device. Because a malicious actor could extract the secret from the client-side code, Keycloak forbids the use of client secrets for public clients. Instead, it mandates the use of PKCE (Proof Key for Code Exchange).
Here is the mechanism of PKCE in action:
- The SPA generates a random code verifier and derives a code challenge.
- It sends the challenge to Keycloak during the authorization request.
- After user authentication, Keycloak issues an authorization code.
- The SPA exchanges the code for a token by sending back the original verifier.
- Keycloak hashes the verifier and compares it to the stored challenge.
This mechanism prevents authorization code interception attacks. Without PKCE, an attacker who intercepts the authorization code could exchange it for a token. With PKCE, they cannot because they do not possess the verifier. Understanding this distinction is not optional; it is the difference between a secure implementation and a vulnerability. For technical details, refer to the OAuth 2.0 / OIDC Specifications on PKCE.
// Example: Generating a PKCE code verifier and challenge
const generateRandomString = (length) => {
const array = new Uint32Array(length / 2);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).slice(-2)).join('');
};
const codeVerifier = generateRandomString(32);
// Simple base64url encoding of the SHA-256 hash
const codeChallenge = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(codeVerifier)
);
const base64Url = btoa(String.fromCharCode(...new Uint8Array(codeChallenge)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');The Underlying Data Model
The data model in Keycloak is relational, typically backed by PostgreSQL or MySQL, but abstracted via Hibernate ORM. Keycloak runs on Quarkus, a Java framework optimized for native compilation and low memory footprint. Understanding this mapping is crucial for performance tuning and debugging.
The core entities are:
REALM: The root container.USER_ENTITY: Stores user attributes and credentials. Note that passwords are not stored in plaintext; they are hashed using PBKDF2 or Argon2, depending on the realm’s password policy.CLIENT: Stores client configuration, including redirect URIs, secret hashes, and protocol mappers.KEYCLOAK_ROLE: Roles are attached to clients or realms. A role is not a user attribute; it is a permission label that can be assigned to users or groups.KEYCLOAK_GROUP: Users are members of groups. Groups can inherit roles from their parent group, creating a hierarchical permission model.
The data model supports composite keys and many-to-many relationships. For example, a user can have multiple roles across different clients. The USER_ROLE_MAPPING table links USER_ENTITY to KEYCLOAK_ROLE. This design allows for fine-grained access control but introduces complexity in query performance. When Keycloak validates a JWT, it does not hit the database for every claim. Instead, it uses a cache layer (infinispan or local LRU) to store recently resolved role mappings.
However, this caching introduces eventual consistency challenges. If you update a user’s role in the database, the change is reflected in the cache after a short delay (configurable via the cache’s eviction and expiration settings). In production environments, this means that role changes are not instantaneous. Engineers must account for this latency when designing access control logic. For instance, a user might still have access to a resource for a few seconds after their role has been revoked, depending on the cache TTL. For deeper technical details, see the Keycloak Data Model Documentation and Hibernate ORM Mapping.
Conclusion
The keycloak architecture is a carefully balanced system of isolation, trust, and persistence. The Realm provides logical separation, ensuring that identity data remains siloed. The Client defines the cryptographic and protocol boundaries, enforcing security through mechanisms like PKCE. The underlying data model, optimized for Quarkus, provides the structure for these concepts but introduces considerations around caching and consistency.
For platform engineers, the takeaway is clear: do not treat Keycloak as a simple login widget. Treat it as a distributed system with explicit boundaries. Configure your realms with isolation in mind, choose your client types based on security requirements, and understand the data model’s caching behavior to avoid subtle race conditions in access control. For a comprehensive overview of these principles, refer to the Keycloak Architecture Overview.
Related posts
Keycloak Client Scopes and Protocol Mappers Explained
A detailed look at Keycloak client scopes and protocol mappers for token customization and claim management.
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.
Implementing WebAuthn in Keycloak: Passkey Authentication Setup
A walkthrough for configuring WebAuthn and passkeys within Keycloak to enable passwordless authentication using FIDO2 standards.