Keycloak wallet proxy
The hosted wallet authorization server federates the OAuth2 authorization-code and PKCE flow out to an external Keycloak realm. It authenticates the user in Keycloak, records normalized authentication evidence from the verified ID token and UserInfo, and returns the wallet-facing token. What becomes a credential attribute is decided afterward, by a separate governed issuance-pipeline step together with the credential design; the proxy itself does not write credential claims. The wallet never talks to Keycloak directly and never learns that Keycloak is the identity source.
Audience: platform operator or tenant administrator for the configuration steps, the wallet for the protocol steps that follow.
Prerequisites: Onboard a tenant and a confidential tenant application, plus a Keycloak realm you control with a confidential OIDC client registered for the Standard Flow.
What this is not
This guide federates a VDX-hosted authorization server out to Keycloak's own OIDC login, so Keycloak authenticates the user and the hosted authorization server issues the downstream wallet token. The Auth Bridge is the opposite direction: it lets Keycloak, or another OAuth2/OIDC authorization server, call into an OID4VP verifier so a wallet-based credential presentation becomes a login factor for that authorization server. If you are trying to let Keycloak accept a wallet credential as a sign-in method, use Auth Bridge instead. If you are trying to let a wallet sign in through an existing Keycloak realm on the way to issuance, this guide is the right one.
The tenant's default authorization server is not affected by any of this. Keep it
LOCAL_ONLY and create a separate hosted authorization server for the proxy, so local
administrator sign-in keeps working regardless of Keycloak's availability.
The three authentication modes
authenticationMode lives on a hosted authorization-server resource, never on an external
one. An external resource is always the federation target, not the mode holder.
LOCAL_ONLYpresents only local authentication against the hosted resource's own credential store. This is the required default for the tenant's primary authorization server.FEDERATED_ONLYhas no local sign-in route. Exactly one eligible enabled federation binding redirects automatically; more than one presents an ordered chooser. The wallet-proxy authorization server uses this mode, with Keycloak as its one binding, so the wallet is sent straight to Keycloak.HYBRIDpresents local authentication and the upstream federated choices together, so an operator can offer both a local account and one or more upstream providers side by side.
Binding order controls chooser ordering whenever more than one upstream is eligible. Issuer,
credential, and offer policy select which authorization server a given offer uses, with offer
precedence deciding ties, so the wallet never picks an arbitrary authorization server on its own.
The configuration operations
These eleven calls set up the proxy end to end: a hosted authorization server dedicated to the proxy, the public wallet client on it, the external Keycloak resource, the federation binding that connects them, and the OID4VCI issuer wiring that routes an offer through the proxy. Run them in order; each step's response feeds the next.
1. Create the hosted proxy authorization server
Create the resource with deployment: HOSTED, authenticationMode: FEDERATED_ONLY, a
dedicated slug such as wallet-proxy, and purposes CREDENTIAL_ISSUANCE and WALLET_LOGIN.
Keep the tenant's existing default authorization server untouched.
2. Register the public wallet client
Register a public authorization-code client with PKCE on the hosted proxy authorization server,
using the wallet's own redirect URI. The response's clientId is what the wallet sends when it
starts the authorization-code flow in the wallet-facing steps below.
3. Register the Keycloak realm as an external resource
This step calls the same createAuthorizationServer operation shown in step 1, but with a
different body and against a different resource: deployment: EXTERNAL, the Keycloak realm's
issuer, and usages: ["HOSTED_LOGIN_UPSTREAM"]. It is one operation invoked twice for two
distinct purposes, not one call that creates both the hosted proxy and the external resource
together; run it once with deployment: HOSTED for step 1 and once with deployment: EXTERNAL
for this step. Creation runs discovery against the Keycloak issuer and reconciles it before the
resource commits, so a discovery failure at this step leaves no partial resource.
4. Read the external resource back
Read the external Keycloak resource by its UUID and check its issuer, deployment, and that no
slug was assigned. An external resource never gets a hosted route, so any slug on it, including
an empty string, is rejected at creation.
5. Validate its discovery snapshot
Validate the external resource's recorded discovery snapshot: issuer consistency, supported capabilities, grants, scopes, endpoints, and client authentication methods. Inspect the result before activating; a stale or invalid snapshot blocks a federation binding from becoming enabled later.
6. Activate the external resource
Activation is required before the resource can be selected by a federation binding. It does not override discovery freshness, so keep the snapshot current.
7. Create the federation binding, disabled
Create the binding below the hosted proxy authorization server, pointing at the external Keycloak resource's UUID. Set the requested scopes, the claims mapping from Keycloak's source claims to the governed authentication targets, and the client authentication method with its write-once credential. Create it disabled; nothing routes through it until it is validated and explicitly enabled.
8. Validate the binding
A binding can only be enabled once both resources belong to the same tenant, the hosted side is active, the external side is active with fresh discovery, every requested scope is supported by Keycloak's discovery, and the chosen client authentication method is advertised. This call checks all of that against the current binding configuration.
9. Enable the binding
Enabling makes the binding live: with the proxy authorization server in FEDERATED_ONLY mode
and exactly one enabled binding, an authorization request against it now redirects straight to
Keycloak.
10. Bind the proxy authorization server to the OID4VCI issuer
Bind the hosted proxy authorization server's UUID to the OID4VCI issuer for the authorization-code grant. This is what makes the proxy an authorization server the issuer will accept for that grant type, separate from whichever authorization server the issuer used before.
11. Select the proxy for a credential configuration
Override the authorization server for a specific credential configuration so an offer for that configuration uses the wallet-proxy authorization server ahead of the issuer's default binding. This is what determines, by offer precedence, that a given credential configuration's offer routes through Keycloak rather than through the tenant's default authorization server.
With the override in place, create the authorization-code offer for that credential
configuration. The response carries the issuer_state the wallet uses to resolve the offer in
the next section.
The wallet-facing steps
Everything from here on is standards-based OID4VCI and OAuth2/OIDC protocol traffic between the wallet, the hosted proxy authorization server, and Keycloak. None of it is modeled as a custom operation in the EDK or IDK specs: the session spec that defines offer creation states directly that the wallet-facing protocol endpoints are a separate surface it does not describe. Do not look for an operation id for any of the steps below; there is not one to find.
The wallet resolves the offer it received (GET the offer URI returned by the previous step),
then discovers the hosted proxy authorization server's OAuth2/OIDC metadata at
GET /as/{authorizationServerSlug}/.well-known/openid-configuration, and separately discovers
the credential issuer's OID4VCI metadata. Both are standard discovery documents, generated per
resource rather than defined as a path in any spec.
The wallet then starts the authorization-code flow with PKCE against the proxy authorization
server using the client registered in step 2. Because the proxy is FEDERATED_ONLY with one
enabled binding, the proxy redirects the wallet straight to Keycloak. The user signs in at
Keycloak, which is a browser interaction with Keycloak's own login UI, not something VDX or EDK
renders. Keycloak redirects back to the proxy's fixed federation callback route, of the form
https://<tenant-gateway>/as/wallet-proxy/federation/callback. The proxy validates the exact
issuer, ID token signature, audience, authorized party, nonce, timestamps, endpoint origin,
selected binding, and transaction before resuming the downstream wallet authorization request
and issuing its own authorization code.
The wallet exchanges that code at the proxy's token endpoint the same way it would for any
other authorization-code grant.
Before requesting the credential, the wallet fetches a fresh holder-proof nonce from the issuer's nonce endpoint and signs a holder proof with its own key, using the credential issuer as audience. It then sends the credential request to the issuer's credential endpoint with that proof and the access token from the previous step. The response contains the issued credential in the format the selected credential configuration defines.
There are no recorded call captures for these wallet-facing steps yet. Once captures exist for
this flow, they belong here as CapturedCall snapshots; until then, treat the descriptions above
as the authoritative sequence rather than a runnable example.
Related
Hosted sign-in and external federation, Auth Bridge, OpenID4VCI overview