You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Default OIDC client scopes: map personal claims to ID token and userinfo only (27.0)
#53800
Keycloak creates the OpenID Connect scopes profile, email, address and phone for every new realm (OIDCLoginProtocolFactory.createDefaultClientScopesImpl()). Every protocol mapper in these scopes is configured with Add to ID token, Add to access token, Add to userinfo and Add to token introspection. This covers name, given_name, family_name, email, email_verified, address, phone_number and the remaining standard claims of profile.
So by default, the same personal data ends up in four places. I'd like to propose limiting these default mappers to the ID token and the userinfo endpoint, targeting Keycloak 27.0. A new major release is the right moment to change defaults in a breaking way. I'd like to hear opinions before opening an enhancement request.
This is only about the defaults. No functionality is removed: every mapper keeps its switches, and anyone who needs personal data in the access token or in introspection can turn them back on.
Why
These are OpenID Connect scopes, not OAuth 2.0 scopes.
They describe what the client learns about the user. OIDC Core, section 5.4, says the claims requested by these scopes are returned from the userinfo endpoint, and in the ID token when no access token is issued.
The access token and the token introspection endpoint are OAuth 2.0 artifacts, and their audience is the resource server.
The access token travels further than the client.
The client forwards it to resource servers, possibly further down a call chain or into a token exchange. With today's defaults, every resource server receives the user's name, email address, postal address and phone number, whether it needs them or not.
The same happens through token introspection.
Data minimisation (GDPR). Personal data should only reach parties that need it for their purpose (Art. 5(1)(b) and (c) GDPR), so sending it to every resource server by default may conflict with that. Secure and privacy-friendly defaults put the burden on the admin who wants more data, not on the admin who has to notice it and remove it.
Duplication and token size. The same data in four places also means larger tokens, which matters for headers and cookies.
If a resource server really needs a personal claim, the admin can still enable Add to access token and/or Add to token introspection on the mapper. Alternatively, the resource server can call the userinfo endpoint with the access token.
Existing alternatives, and why they aren't enough
Lightweight access tokens (client setting client.use.lightweight.access.token.enabled) strip claims from the access token per client. But introspection still returns the full set by design, and it's opt-in for each client, not a default.
Removing the flags manually in every realm works, but it's easy to miss, and every new realm starts over with the current defaults.
Proposal
New realms: the default mappers of profile, email, address and phone set only Add to ID token and Add to userinfo. In the admin console, "Add predefined mapper" uses the same built-in definitions (getBuiltinMappers()), so it follows the new defaults.
Introspection follows the access token: a claim that isn't in the access token isn't in the introspection response either. Introspection is the resource server's channel, so excluding personal claims from the access token but not from introspection would defeat the purpose.
Exception, preferred_username: it stays in the access token and in introspection as today. Many resource servers use it as the principal name, and it identifies the user rather than describing them.
Existing realms stay untouched. The scopes are created once, together with the realm, so existing deployments keep their configuration and nothing changes on upgrade. There's no automatic migration.
No opt-out option: this is a plain change of the defaults in a major release, documented in the upgrade notes for 27.0. Admins who want the old behaviour turn the switches back on, as with any other mapper configuration. This also applies to realms imported without clientScopes, because they get the built-in defaults.
Out of scope:
the basic scope (sub, auth_time);
the role, audience and web-origins mappers.
These are authorization-related and belong in the access token.
Trade-off: lightweight access tokens
With lightweight access tokens, resource servers get user data through introspection on purpose, because the token itself carries almost no claims. After this change, such setups need to enable Add to token introspection on the mappers they rely on. They can also call userinfo instead, but that only works when the access token carries the openid scope. This is a one-flag change per mapper. That's an acceptable price for a privacy-friendly default in a major release.
What I checked in the code base
To estimate the effort, I looked at what depends on these claims in the access token and in the introspection response.
The change itself is small. It's limited to the built-in mapper definitions in OIDCLoginProtocolFactory.initBuiltIns(), plus FullNameMapper.create(...) and AddressMapper.createAddressMapper().
Server code: no dependency.
The admin console and account console read user claims from the ID token; from the access token they only read roles.
OIDC brokering reads the ID token and the userinfo endpoint.
The internal fallback lookups by preferred_username are unaffected, because preferred_username stays in the access token.
Tests: this is where the effort is.
65 of the 78 realm files in the test suites, including the main testrealm.json, don't define their own client scopes, so they get the built-in defaults.
Keeping preferred_username removes most of the impact. A static scan finds roughly 15–20 test classes that read other personal claims (mostly email) from the access token or from the introspection response, e.g. the duplicate-email tests, the Kerberos multi-realm tests, pairwise client registration and the introspection checks in AccessTokenTest.
A few of them test the claims in the access token on purpose (OIDCProtocolMappersTest, LightWeightAccessTokenTest, ClientScopeEvaluateTest, parts of StandardTokenExchangeV1Test). They would either assert the new defaults or enable the flags explicitly.
The adjustments look mechanical: assert on the ID token or userinfo instead, or add the access token flag where a test needs the claim there.
Docs: the server admin guide (client scopes, protocol mappers) and an upgrade note.
Feedback welcome
Is there a use case I'm missing that would make these defaults a problem for new realms? I'm especially interested in setups that rely on personal claims in the access token or in introspection out of the box.
Generative AI disclosure
Claude Code was used to analyse the current default client scopes and protocol mappers, to check which server code and tests depend on personal claims in the access token and the introspection response, and to draft this discussion. I reviewed the content before posting.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page 8000 .
Keycloak creates the OpenID Connect scopes
profile,email,addressandphonefor every new realm (OIDCLoginProtocolFactory.createDefaultClientScopesImpl()). Every protocol mapper in these scopes is configured with Add to ID token, Add to access token, Add to userinfo and Add to token introspection. This coversname,given_name,family_name,email,email_verified,address,phone_numberand the remaining standard claims ofprofile.So by default, the same personal data ends up in four places. I'd like to propose limiting these default mappers to the ID token and the userinfo endpoint, targeting Keycloak 27.0. A new major release is the right moment to change defaults in a breaking way. I'd like to hear opinions before opening an enhancement request.
This is only about the defaults. No functionality is removed: every mapper keeps its switches, and anyone who needs personal data in the access token or in introspection can turn them back on.
Why
If a resource server really needs a personal claim, the admin can still enable Add to access token and/or Add to token introspection on the mapper. Alternatively, the resource server can call the userinfo endpoint with the access token.
Existing alternatives, and why they aren't enough
client.use.lightweight.access.token.enabled) strip claims from the access token per client. But introspection still returns the full set by design, and it's opt-in for each client, not a default.Proposal
New realms: the default mappers of
profile,email,addressandphoneset only Add to ID token and Add to userinfo. In the admin console, "Add predefined mapper" uses the same built-in definitions (getBuiltinMappers()), so it follows the new defaults.Introspection follows the access token: a claim that isn't in the access token isn't in the introspection response either. Introspection is the resource server's channel, so excluding personal claims from the access token but not from introspection would defeat the purpose.
Exception,
preferred_username: it stays in the access token and in introspection as today. Many resource servers use it as the principal name, and it identifies the user rather than describing them.Existing realms stay untouched. The scopes are created once, together with the realm, so existing deployments keep their configuration and nothing changes on upgrade. There's no automatic migration.
No opt-out option: this is a plain change of the defaults in a major release, documented in the upgrade notes for 27.0. Admins who want the old behaviour turn the switches back on, as with any other mapper configuration. This also applies to realms imported without
clientScopes, because they get the built-in defaults.Out of scope:
basicscope (sub,auth_time);These are authorization-related and belong in the access token.
Trade-off: lightweight access tokens
With lightweight access tokens, resource servers get user data through introspection on purpose, because the token itself carries almost no claims. After this change, such setups need to enable Add to token introspection on the mappers they rely on. They can also call userinfo instead, but that only works when the access token carries the
openidscope. This is a one-flag change per mapper. That's an acceptable price for a privacy-friendly default in a major release.What I checked in the code base
To estimate the effort, I looked at what depends on these claims in the access token and in the introspection response.
OIDCLoginProtocolFactory.initBuiltIns(), plusFullNameMapper.create(...)andAddressMapper.createAddressMapper().preferred_usernameare unaffected, becausepreferred_usernamestays in the access token.testrealm.json, don't define their own client scopes, so they get the built-in defaults.preferred_usernameremoves most of the impact. A static scan finds roughly 15–20 test classes that read other personal claims (mostlyemail) from the access token or from the introspection response, e.g. the duplicate-email tests, the Kerberos multi-realm tests, pairwise client registration and the introspection checks inAccessTokenTest.OIDCProtocolMappersTest,LightWeightAccessTokenTest,ClientScopeEvaluateTest, parts ofStandardTokenExchangeV1Test). They would either assert the new defaults or enable the flags explicitly.Feedback welcome
Is there a use case I'm missing that would make these defaults a problem for new realms? I'm especially interested in setups that rely on personal claims in the access token or in introspection out of the box.
Generative AI disclosure
Claude Code was used to analyse the current default client scopes and protocol mappers, to check which server code and tests depend on personal claims in the access token and the introspection response, and to draft this discussion. I reviewed the content before posting.
All reactions