Skip to main content
Version: v0.25.0 (Latest)

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_ONLY presents only local authentication against the hosted resource's own credential store. This is the required default for the tenant's primary authorization server.
  • FEDERATED_ONLY has 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.
  • HYBRID presents 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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Loading example...

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.

Hosted sign-in and external federation, Auth Bridge, OpenID4VCI overview