Discuss a project
menu

Wallet login in Keycloak beyond pinned issuer keys

What keycloak-oid4vp 0.2.0 adds to wallet login on Keycloak 26.8: ETSI trust lists, issuer DIDs, revocation checks, same-browser wallets, EUDI registration certificates and issuer-scoped identities.

Keycloak 26.8 can verify wallet presentations over OpenID4VP on its own, as an experimental feature. We covered how to set it up in Setting up Keycloak 26.8 as an EUDI credential issuer and verifier. Its trust model is a set of pinned issuer keys. That works for a pilot with one issuer. It stops working once a relying party has to accept credentials from every issuer a national or EU trust list names, or from issuers that publish their keys as DIDs.

keycloak-oid4vp is our open-source Keycloak extension for wallet login. Version 0.2.0 targets Keycloak 26.8.0 and 26.8.1. This article walks through what it adds and why each part matters for a production relying party. The full list of changes is in the changelog.

trust issuers the way they publish themselves

The extension does not impose one trust model. A realm can anchor issuers in X.509 certificates, in DIDs, or in both, depending on how the issuers it accepts are actually run.

For certificate-based issuers, the trust anchor can be:

  • an ETSI TS 119 602 List of Trusted Entities in its JSON or JWT form;
  • an ETSI TS 119 612 XML trusted list, or the EU list of trusted lists, which the extension follows to the national lists, optionally limited to the territories you name;
  • for mDoc credentials, a signed VICAL, such as the AAMVA list of US mobile driving licence issuers.

Every trust list must be signed by a certificate you configure. An XML list's signature must cover the whole document, and each national list is verified with the certificates the EU list names for it. Only services of the types you allow become trust anchors, and no type is allowed by default. A misconfigured realm accepts nothing, which is the failure you want.

An SD-JWT issuer certificate must also name the issuer it signs for: the iss value has to appear in the certificate's subject alternative names, as the SD-JWT VC specification requires. Without that check, any certificate on the trust list could sign credentials in another issuer's name.

For issuers without a PKI, the extension resolves did:web and did:webvh keys. Only keys the DID document lists for assertionMethod may sign credentials. allowedIssuers accepts any issuer by default; set it to the exact DIDs you trust, or any DID that resolves to a valid key is accepted as an issuer. Keys published at /.well-known/jwt-vc-issuer are accepted only when their certificate chain reaches a configured trust anchor. The issuer allowlist is applied before anything is fetched for a credential.

Status lists, DID documents and issuer metadata are fetched from URLs that arrive inside presented credentials. By default Keycloak only fetches them from public addresses. If your issuers run on an internal network, enable allowPrivateNetworkFetch for that identity provider.

check that a credential is still valid

A valid signature says the credential was issued. It does not say the credential is still valid. The extension checks two things.

Credential status comes from the Token Status List the credential points to, in JWT or CWT form, for both SD-JWT and mDoc credentials. The list's signature is always verified, against the trust list or the keys that verified the credential.

Issuer certificates can be checked for revocation with OCSP and CRL. certificateRevocationCheck takes none, soft or strict. soft accepts a certificate whose status cannot be determined, for example when the responder is down, and strict rejects it. Which one is right depends on whether an unreachable OCSP responder should block logins. Make that decision deliberately, and record it.

let people use a wallet in the same browser

Cross-device login with a QR code works, but it is awkward when the wallet is on the same phone or laptop as the browser. The W3C Digital Credentials API lets a web page ask the browser for a credential directly. With dcApiEnabled, the login page shows "Use a wallet in this browser" in browsers that support the API.

Requests follow OpenID4VP 1.0 Appendix A. They are signed when the client ID scheme is bound to a certificate and the realm has a verifier certificate, and every presentation is bound to the origin of your login page, so a response made for another site is rejected here. That origin comes from configuration, dcApiOrigin or the realm's Frontend URL, never from request headers. Without either, the button is not shown.

For mDoc requests, the extension also offers ISO/IEC 18013-7 Annex C, the protocol Safari and Apple Wallet use. Responses are HPKE-encrypted and bound to the origin. Apple Wallet only answers requests signed with a certificate issued through Apple Business Connect. We have tested this path against our own Annex C test wallet, not yet against Apple Wallet on a device.

tell the wallet who is asking

Under the EUDI Wallet framework, a relying party registers which credentials and attributes it intends to request, and wallets can check a request against that registration. The extension sends your registration certificate (ETSI TS 119 475) in verifier_info with every request.

At startup, Keycloak logs a warning when the certificate has the wrong type, an invalid signature or has expired, or when it does not belong to the same organisation as the access certificate. It also warns when the configured query asks for credentials or claims the certificate does not register. That last check catches over-asking in your logs, before a wallet refuses the request in front of a user.

keep identities from different issuers apart

Keycloak links a wallet user to an account through an identifier claim, such as a personal identifier or a document number. By default, the same value reaches the same account whichever trusted issuer presented it. That is what you want when one person may log in with an SD-JWT credential one day and an mDoc credential the next.

It is not what you want when your trust list holds issuers whose identifiers can collide. Document numbers from different authorities are an example, and a VICAL of every US state is a realistic case. With identityKeyScope=issuer, the account link also covers who issued the credential. Each login records its issuer on the user, and after you switch, existing accounts move to the new link on their next login only when the credential comes from the issuer they used before.

run it next to Keycloak's own verifier

Keycloak 26.8 uses the provider ID oid4vp for its native verifier. The extension now uses oid4vp-wallet, so both can be installed on the same server. Realm aliases, broker URLs, mappers and linked identities stay the same.

what we tightened before the release

We reviewed the full verification path before tagging this version. Among the changes:

  • mDoc claims are read only from the items whose digests the issuer signed.
  • Same-device logins require a one-time code that only the browser returning from the wallet holds, and every wallet response can be used once.
  • Error pages and wallet error responses show a fixed message; the reason goes to Keycloak's event log.
  • The verifier's private key can be read from Keycloak's vault instead of the identity provider configuration.
  • Certificate chains are checked for name chaining, key usage and path length, and caches are separated per configuration and bounded in size.

The changelog lists every change.

upgrading an existing deployment

The previous tagged version targeted Keycloak 26.5. Version 0.2.0 requires Keycloak 26.8.0 or 26.8.1 and changes some defaults:

  • Run scripts/migrate-provider-id.sql for the provider ID rename. Keycloak's admin API cannot change a provider ID, and recreating the provider would delete linked identities.
  • A trust list without a configured signing certificate is now rejected instead of trusted as fetched.
  • did:web URLs follow the did:web specification. Set didWebLegacyPathLayout=true if your issuers still use the previous layout.
  • SD-JWT issuer certificates must name their iss value. x5cIssuerBinding=false exists only for issuers that cannot reissue certificates in time, and should be combined with allowedIssuers.
  • Set dcApiOrigin to offer wallets in the same browser, and allowPrivateNetworkFetch if issuers serve status lists or DID documents from an internal network.
  • Move the verifier private key into Keycloak's vault: anyone who can view identity providers in the admin console can read a key stored inline.
  • The cross-device page now polls for completion instead of holding a server-sent event stream open. Custom themes that copied the old script need the new one.

The migration guide has the steps and a rollback plan.

limits

enforceHaip switches to a HAIP-oriented configuration (direct_post.jwt responses and x509_hash client IDs). It is not a certification, and DID resolution runs outside it. We have not run the Annex C flow against Apple Wallet or the registration certificate against a wallet in ARF mode. SD-JWT claims disclosed as array elements are not yet resolved for claim mapping, and DCQL trusted_authorities is sent to wallets but not enforced on responses. The enterprise readiness plan lists the security and operational work that remains. Test with the wallets and issuers you will actually accept before going live.

wallet login is a trust decision

Adding a wallet button to a login page takes an afternoon. Deciding which issuers to trust, how to learn about revocation, and what to do when a trust list or responder is unreachable takes longer, and those decisions determine whether the login can be relied on.

We help teams connect EUDI wallets to the identity systems they already run. See our identity project work, or tell us what you are building.

Working on something similar? Talk to us

← all writing