When someone signs in to your app with a social or enterprise account — Google, Facebook, a company IdP — Keycloak brokers that login and stores whatever the identity provider sent back as brokered user attributes: email, name, and so on. Those claims are usually only good for stuffing a few attributes into the user profile. But what an IdP delivers is not a contract: it can change, or be missing entirely, and then your realm has to decide what to do.
Take Facebook login, the classic example. Facebook’s email permission is
optional — users can grant or deny
the permissions an app requests, and if a user declines email during OAuth consent,
Facebook simply does not deliver an email claim to your app. Even with the permission
granted, Facebook only returns the email associated with the account;
a user who signed up with a phone number and has no email on file yields an empty email
field.
Worse, the absence of an email is not always the user’s choice in that moment. There is a
long-standing, widely-reported Facebook bug where if the account’s original primary email
is ever removed — even if the user re-adds that same address, or adds new verified ones —
the Graph API stops returning an email to apps, seemingly forever (Facebook developer
forum,
another report,
tracked on Supabase’s auth). The email is
still on the account, still verified, and the user still grants email access — the field
just never comes back. It is exactly the kind of “missing from nowhere” scenario you
cannot debug from the app side, because everything on the app side looks correct.
Either way, your Keycloak realm ends up with a brokered login that has no email, and flows that assumed one is present start to misbehave.
keycloak-idp-claim-utilities
is a small Keycloak extension with three authenticators designed for exactly this: check
the claims a broker actually delivered, and branch the login flow on them — e.g. deny
access when a required claim like email is missing, or when it does not match what you
expect. It is provider-agnostic: it works with any IdP that delivers its claims as
brokered user attributes.
Authenticators
| Provider ID | Type | What it does |
|---|---|---|
claim-presence | Conditional | Matches when a configured claim is present (non-blank). |
claim-value | Conditional | Matches when a claim’s value equals an expected value (optionally a regex). |
deny-redirect | Regular | Terminates login, redirecting back to the app’s redirect URI with ?error=<code>. |
The conditional authenticators read the brokered context from both the first-broker-login and post-broker-login flows, and each supports an optional Negate output toggle to invert its result.
claim-presence
Matches when the configured claim is present with at least one non-blank value. This is
the one you want for the Facebook email case above: gate the flow on email being present,
and route the “no email” branch to denial. The Negate output toggle inverts it, e.g.
to match when a claim is absent.
claim-value
Matches when a claim’s value equals an expected value. Without regex, at least one of the claim’s values must match exactly; with Match as regex enabled, any value containing the pattern passes. Use it to allow only users whose email domain matches, for example.
deny-redirect
The terminal authenticator for the “deny” branch — where you would otherwise put Keycloak’s
built-in Deny Access. Unlike Deny Access, which shows Keycloak’s own access-denied
page, it redirects the browser back to the client’s redirect URI with a stable code in the
error querystring parameter: the same redirect shape as a successful login, but carrying
the error instead of an authorization code.
The Error code dropdown offers standard OAuth2/OIDC authorization-error codes so the client app can map a code to a localized message instead of rendering raw text:
| Code | Meaning |
|---|---|
access_denied (default) | Generic deny; fallback for any failed claim check. |
claim_missing | A required identity claim (e.g. email) was absent from the IdP login. |
interaction_required | Additional user interaction needed before access is granted. |
invalid_request | Request missing a required parameter or otherwise malformed. |
invalid_scope | Requested scope is invalid, unknown, or malformed. |
server_error | Server-side failure. |
unauthorized_client | Client not permitted to use this flow. |
Configs holding an older free-text value (not one of the above) are still honored — the raw value is passed through rather than replaced.
Installing
The extension ships as a single JAR published to Codeberg Packages. Add it to a Keycloak
container image by placing it in /opt/keycloak/providers/ (the default provider
directory): in a Dockerfile, fetch it from the Maven repository during the build,
substituting the version you want for <version>:
ADD --chown=keycloak:keycloak --chmod=644 \
https://codeberg.org/api/packages/khanetor/maven/me/khanetor/keycloak-idp-claim-utilities/<version>/keycloak-idp-claim-utilities-<version>.jar \
/opt/keycloak/providers/keycloak-idp-claim-utilities.jar
Then start Keycloak as usual; the new provider IDs appear in the authentication flows or identity-provider settings. To see the available versions, browse the Maven package; for build and development details, see the repository README.
Why deny-redirect over Deny Access
The useful bit is the redirect. Keycloak’s Deny Access ends login at its own “Access denied” page, whose text the user has to read. Redirecting back to the app keeps the flow in your app’s hands — your UI renders whatever the error code means in this context, in the user’s own language, and can offer alternatives (e.g. “log in with a different provider”) instead of a dead end.