Last updated: July 2026
TL;DR
Adopting Keycloak does not make Keycloak your user database. In most enterprises, identity is provisioned somewhere else (Microsoft Entra, a provisioning service, an AD, a legacy DB), and Keycloak keeps a shadow copy so it can authenticate people and hand out a JWT. Authorization is then somebody else’s call. The short version:
- Keycloak stores users, but “stores” is not “owns.” In a federated setup, the row in Keycloak’s database is a local projection of a user whose truth lives upstream.
- A shadow account is created on first login, not on hire. Unless something provisions ahead of time, Keycloak learns a user exists the moment they show up at the door.
- Three jobs, three systems. Source of truth (Entra), authenticator (Keycloak), authorizer (the service provider). Teams collapse these into one box and then wonder why nothing reconciles.
- Drift is the tax you pay for the shadow copy. Stale attributes and orphaned accounts are the default outcome unless you deliberately design against them.
- Stuffing every role into the token is a trap. It ends in bloat, stale claims, and eventually an HTTP 431.
We run managed Keycloak for teams wiring exactly this pattern, so consider this the architecture conversation we have before anyone touches a config screen.
Scope note. This post is about ownership boundaries, not clicks. If you want the actual walkthrough, we have one on setting up Entra ID SAML in Keycloak as an IdP, and another on attribute mapping during OIDC identity brokering. Go there for the steps. Stay here for the part that decides whether those steps were even the right ones.
Does Keycloak store your users?
Yes, technically. Keycloak has a database, and after someone logs in through a federated identity provider there is a real user row in it, with a username, an email, and a link back to the upstream provider. But in most federated enterprise setups, that row is a shadow copy, not the source of truth. Keycloak is repeating what Entra told it.
A senior IAM engineer at a large European retail group put it about as bluntly as it can be put:
“Keycloak itself does not provide identities. So it only has shadow accounts.”
That sentence is worth re-reading if you are about to build a joiner-mover-leaver process around Keycloak’s admin API. Keycloak does not know who was hired. It does not know who was terminated, who changed departments, or who is on parental leave. It knows who successfully logged in and what the upstream IdP asserted about them at that exact moment. Everything else is inference.
So the real question is not “does Keycloak store users.” It’s “who gets to change them?” If the answer is anything other than Keycloak, then Keycloak is a broker, and you should design it like one.
What is a shadow account, actually?
A shadow account is the local user record Keycloak creates to represent a user whose identity is owned by an external provider. It exists so Keycloak has something to attach sessions, roles, consents, and a federated identity link to. It is a projection, and like every projection, it is only as fresh as the last time it was refreshed.
When does it get created?
Two moments, and the difference matters more than most teams expect.
On first login (the default). Someone clicks the Entra button, authenticates upstream, comes back, and Keycloak runs its first broker login flow: it checks whether it already knows this user, links or creates the account, and lets them through. Before that click, the user does not exist in Keycloak. Not as a disabled record, not as a placeholder. Nothing.
On provisioning (the deliberate choice). Something pushes the user into Keycloak ahead of time, typically via SCIM or the admin API, so the account exists before the first login. This is what you want if you need to assign entitlements in advance, or if you need deprovisioning to actually work. More on that below.
Most teams end up on first-login-only by accident, because it works on day one and requires zero extra plumbing. The bill arrives later.
What does it hold, and what does it not?
The shadow account holds the boring, useful stuff: a username, email, first and last name, whatever attributes your mappers copied over, a link to the upstream identity, active sessions, and any roles or group memberships you assigned locally. It is enough to authenticate a person and describe them in a token.
Here is what it does not hold, and this is the part that gets skipped:
- Employment status. Keycloak has no idea the person left in March.
- HR structure. Manager, cost center, department, contract type. Unless you mapped it, and even then it’s a copy with a timestamp you’re not tracking.
- The password. In a pure broker setup, there is no local credential. Keycloak never sees it, which is exactly the point.
- Authoritative group membership. It holds what Entra said at last sync. Not what Entra says right now.
- Downstream entitlements. What the user can do inside your billing app is the billing app’s business.
A shadow account is a cache with better branding. Treat it as such and most of the architecture decisions make themselves.
Which system owns what: the three roles teams conflate
Three distinct jobs get collapsed into “auth” in almost every planning doc, and the collapse is where the mess starts. Split them out and say them out loud:
| Role | Who does it | The question it answers |
|---|---|---|
| Source of truth | Entra ID, a provisioning service, AD, a legacy DB | Does this person exist, and who are they? |
| Authenticator | Keycloak | Is this person really them, right now? |
| Authorizer | The service provider (your app/API) | Is this person allowed to do this specific thing? |
The same engineer described the boundary in one sentence:
“Keycloak is there basically only for authentication to give the service provider JWT token, and then it’s up to the service provider to decide if the claims are fitting authorization or not.”
Read that as a contract, not a limitation. Keycloak’s job ends at “here is a signed, current, trustworthy statement about who this is and what the IdP said about them.” What your app does with that statement is your app’s problem, and it should be, because your app is the only thing that knows what its own permissions mean.
Notice what this framing kills off. It kills the idea that the IAM team is a ticket queue for “please add a role so my feature works.” It kills the fantasy of one global role model that every product team agrees on. And it gives the IAM team a defensible line: we assert identity, you decide access.
What breaks when shadow accounts drift?
Drift is the gap between what Entra says today and what Keycloak’s copy said the last time it looked. It is not a bug, it is the natural state of any cache you don’t invalidate. In brokered Keycloak, drift shows up in three flavors, each with its own blast radius.
Stale attributes. Someone changes department, gets a new email, or moves to a different legal entity. Entra knows. Keycloak’s shadow copy still says what it said at first login, because by default an identity provider imports attributes once and then leaves them alone. Keycloak does expose a sync mode you can set to refresh on every login, and it’s usually the right call, but it’s a decision somebody has to actively make. If your mappers and sync behavior are fuzzy to you, the attribute mapping post covers the mechanics.
Login-gated freshness. Even with refresh-on-login, the copy only updates when the user logs in. A user who hasn’t logged in since January is carrying January’s attributes. Any downstream decision made off that data is a decision made with old news.
Orphaned accounts. The ugly one. The user is disabled in Entra. They can no longer log in through the broker, so it feels solved. But the Keycloak account still exists: still enabled, still holding roles, still referenced by every app that keyed its own records to that user ID. Now your access review exports a user list that includes people who left last quarter, and your auditor asks a question you don’t want to answer.
That last one is why deprovisioning belongs on the provisioning path, not the login path. If the only way Keycloak learns anything about a user is that they showed up, then Keycloak can never learn that they stopped existing. Absence is not an event. You need something to push the leaver signal in, which is precisely the job SCIM exists to do: Entra tells Keycloak “this person is gone” instead of Keycloak waiting for a login that never comes.
Worth knowing on the Keycloak side: the 26.x line ships a native SCIM 2.0 capability as an experimental feature behind the scim-api flag, with Entra compatibility as an early target. It’s real, it’s improving, and it’s still experimental (no bulk operations, no custom schemas yet), so plenty of production setups still lean on mature SCIM extensions. Either way, the architectural point holds: joiner, mover, and leaver events should arrive as events.
Where does authorization belong?
With the service provider, in almost every case. Keycloak can absolutely carry authorization data in the token, and for coarse decisions it should. But the moment you decide the token is where all authorization lives, you’ve signed up for a specific and well-documented kind of pain.
Start with the physics. Every role, every group path, every entitlement you map into the access token makes that token bigger, and it travels in an HTTP header on every single request. Keycloak on Quarkus sits behind Vert.x, which enforces limits on header size, and your reverse proxy has its own limits, usually smaller than you think. Cross them and you get an HTTP 431, which we wrote up in detail in fixing Keycloak’s 431 “Request Header Fields Too Large”. The classic version of this incident: one user in forty groups, and only that user can’t log in.
Now the subtler problem. A token is a snapshot with a signature on it. Revoke someone’s role in Keycloak and their existing access token keeps asserting that role until it expires, because that’s what a signed snapshot is. Short token lifetimes shrink the window, they don’t close it. So the more authorization you push into the token, the more of your access control is running on data that is, by design, slightly out of date.
The rule we’d actually defend: put identity and coarse, slow-moving claims in the token. Keep fine-grained, fast-moving permissions in the service that owns them. Team membership, tenant ID, a handful of broad roles: fine, that’s stable and every service needs it. Whether this user can approve a refund over 500 euros in store 214: that’s the refund service’s decision, made against the refund service’s data, at the moment of the request. Don’t try to precompute it in a JWT at login time.
If you’re mid-migration and your apps have opinions about token shape they inherited from a legacy IdP, that’s a related but separate problem, and we covered it in token and claims modeling for legacy IdP behavior.
Which anti-patterns should you refuse?
These are the four that show up over and over, roughly in order of how expensive they get.
Dual sources of truth. Users can be created in Entra and in Keycloak, and both are “real.” Now nobody can answer “how many employees have access” without a reconciliation script, and the script will find rows neither system admits to. Pick one system that can mint a user. Everything else reads.
Editing users in two places. The subtler cousin. An admin fixes a typo’d email directly in the Keycloak console because it’s faster than filing an Entra ticket. It works, briefly. Then the user logs in, sync mode overwrites it, and the “bug” comes back from the dead. Two hours of debugging follow. If Entra owns the field, then Entra owns the field, including when that’s annoying.
Treating Keycloak as an HR system. Custom attributes for manager, start date, contract type, cost center. It feels harmless because Keycloak will happily store all of it. But now you have HR data with no lifecycle, no owner, and no refresh, sitting in a system whose actual job is issuing tokens. If a field is not needed to authenticate a user or to make a coarse access decision, it does not belong in the shadow account.
Over-stuffed tokens. Full scope allowed left on, every group path mapped in, every client’s roles visible to every other client. It works fine in a demo realm with three users, and it degrades quietly as the org grows until it fails loudly and all at once. Map what the app reads. Nothing else.
Every one of these is the same mistake wearing a different hat: someone let Keycloak’s convenience blur the ownership boundary. The boundary is the design.
Frequently asked questions
Does Keycloak store users, or not?
It stores them. It usually doesn’t own them. In a federated setup Keycloak holds a local user row (a shadow account) that mirrors an identity mastered in Entra, AD, or a provisioning service. It exists so Keycloak can attach sessions, links, and roles. Treat it as a cache of upstream truth, not the record itself.
When is a shadow account created in Keycloak?
By default, at first login through the identity provider. The user authenticates upstream, returns to Keycloak, and Keycloak’s first broker login flow creates or links the local account. If you need the account to exist before the user ever logs in (for pre-assigned entitlements or working deprovisioning), you provision it ahead of time via SCIM or the admin API.
Do shadow accounts get deleted when someone leaves?
No, not on their own. Disabling a user in Entra stops them from logging in, but the Keycloak record stays enabled with its roles intact, because a missing login is not an event Keycloak can react to. You need a provisioning path that pushes the leaver signal in, which is what SCIM handles. Without it, expect orphans at your next access review.
Should authorization live in the Keycloak token or in the application?
Coarse and slow-moving claims (tenant, team, a few broad roles) belong in the token. Fine-grained permissions belong in the service that owns them. Tokens are signed snapshots, so anything in them is stale the instant it’s revoked, and every extra claim grows a header you send on every request. That path ends at HTTP 431.
Why are my user’s attributes wrong in Keycloak but right in Entra?
Almost always sync mode. By default a brokered identity provider imports attributes when the account is first created and leaves them alone afterward, so a user who changed departments in March still carries January’s values. Refreshing on every login fixes most of it, though it only ever updates users who actually log in.
Can Keycloak be the source of truth if I want it to be?
Sure, and for plenty of setups (customer-facing apps, greenfield products, anything without a corporate directory upstream) that’s the right answer. Keycloak is a perfectly good user store. The failure isn’t picking one model, it’s running both at once and never deciding which one wins when they disagree.
Wrapping up
The broker pattern isn’t a compromise or a Keycloak limitation. It’s what a correct enterprise deployment usually looks like: Entra (or whatever owns your directory) decides who exists, Keycloak decides whether they’re really them and says so in a signed token, and each service decides what that token entitles them to. Three systems, three jobs, one boundary between each.
The teams that struggle aren’t the ones that picked the wrong tool. They’re the ones that never drew the boundary, so identity leaked into Keycloak, authorization leaked into the token, and HR data leaked into a system that issues JWTs for a living. Draw the lines first. Then go configure the identity provider, knowing why you’re configuring it that way.