Configuring OpenID4VP and wallet sign-in
This page shows how to set up iya-sts as a
OpenID4VP 1.0
Verifier, how to let people sign in with a wallet, how to accept self-issued
ID Tokens (SIOPv2), and how to publish an application’s DID. Each recipe is
given twice, once as steps in the admin console (/admin) and once as
calls to the Management API (/admin-api), and the user portal steps
where the person does the work.
The values come from this repository’s own test jobs, and each recipe
names the job it is taken from. Change the identifiers and URLs to your own.
How the Verifier and the sign-in behave, and every setting with its
default, is on OpenID4VP, wallet sign-in and DIDs. This page
covers only what to fill in. Issuing the credentials a wallet presents is on
Configuring OpenID4VCI.
Configuring OAuth 2.0 grants → Before you start
explains the shared mechanics: the API token, the api shell helper used
below, and the application attribute editor.
- Configuring OpenID4VP and wallet sign-in
- Where things are
- What development and product mode change
- Choose what the Verifier asks for
- Start a verification and read its outcome
- How a signed request names the Verifier
- Trust another issuer, and the status rules
- Sign in with a wallet
- Offer a web wallet for presentations
- Self-issued ID Tokens (SIOPv2)
- DIDs
- Related
Where things are
| Page | Console menu | What you do there |
|---|---|---|
/admin/vc#vc-tab-oid4vp |
Protocols → Verifiable Credentials, tab OpenID4VP | Every oid4vp.* setting, the wallet sign-in’s among them, saved with Save OID4VP. |
/admin/vc#vc-tab-verifier-request |
the same page, tab Verifier request | What the Verifier asks for, in which format, and the Start a verification form with the Open verifications list. |
/admin/vc/monitor |
Monitoring → Verifiable Credentials | Suspend, reinstate or revoke a credential, which stops it signing in. |
/admin/users |
Directory → Users, a person’s page | Enrol or remove a person’s self-issued IDs. |
/admin/applications?application=<id> |
Directory → Applications | appAuthnPolicy, and an application’s DID. |
/admin/policies#authn-named |
Directory → Policies, tab Authentication policy | The named authentication policies an application may name. |
/portal/wallet, /portal/self-issued |
user portal | A person collects a credential, and enrols their own self-issued ID. |
GET /admin-api/oid4vp-settings reads the settings tab, and
GET /admin-api/verifier-request the Verifier request tab.
GET /oid4vp/verifier-certificate publishes the Verifier’s x509 Client
Identifiers, its chain and its trust anchor.
What the Verifier supports. A request is a DCQL query (dcql_query, with
credential_sets); DIF Presentation Exchange (presentation_definition) is
not supported. It asks for dc+sd-jwt, jwt_vc_json or ldp_vc, and not
mso_mdoc. The response modes are direct_post, direct_post.jwt
(encrypted) and, for SIOPv2, form_post; over the Digital Credentials API
they are dc_api.jwt and dc_api.
What development and product mode change
The Verifier’s checks, the sign-in rules, the browser binding and the status check are the same in both modes. What differs is what a start or a setting may name:
- The wallet a verification start names. Development mode sends a request
to any URL. Product mode sends it only to
oid4vp.walletUrlor one listed inoid4vp.allowedWalletUrls. - The
redirect_uria start names. Development mode accepts any absolutehttporhttpsURL without a fragment. Product mode accepts only one of the calling client’s registered redirect URIs, matched whole (STS-VC-0123). - The
x509_san_dnsname. Withoutoid4vp.x509DnsNameorglobal.publicBaseUrl, development mode uses the host the request arrived at. Product mode refuses the request. oid4vp.requireStatusReference=off. Development mode only. Product mode refuses to set it and reads a storedoffasall.- A password after a wallet. As everywhere, development mode does not check it and product mode does.
- Which credentials sign in. In both modes, only a credential this realm issued on an access token it verified. In development the credential endpoint also issues on a token it did not verify; such a credential still verifies at the Verifier and signs nobody in.
Choose what the Verifier asks for
What the Verifier asks for is configuration, separate from what the issuer
mints. Asking for a claim no credential here carries is allowed: it is how a
wallet’s I cannot satisfy this request path is reached. Taken from
admin_api.js and sts_oid4vp_status_reference.js.
Console:
- Open Protocols → Verifiable Credentials, tab Verifier request.
- Under The claims, tick each claim to ask for in the Ask column, for
example
given_nameandfamily_name. The Issued now column says whether the issuer currently mints it. - Click Save this request.
- To ask for a claim that is not in the catalogue, type it under Also ask for a claim that is not in the catalogue and click Add.
- Under The credential types that can be submitted, choose Ask for this one by default on the format a start uses when it names none.
Back to what this process started with returns to oid4vp.claims
(given_name,family_name by default). The query this builds shows the
next request’s dcql_query.
API:
api verifier-request/select '{"claims":["given_name","family_name"]}'
api verifier-request/add '{"claim":"no_credential_here_carries_this"}'
api verifier-request/remove '{"claim":"no_credential_here_carries_this"}'
api verifier-request/format '{"format":"dc+sd-jwt"}'
api verifier-request/defaults '{}'
select with {"claims":[]} asks for no claim. DCQL reads that as the whole
credential, and sts_oid4vp_status_reference.js uses it so that a credential
which discloses nothing answers the request. The claims are frozen onto each
request when it is built, so a change does not affect a presentation already
in flight.
Start a verification and read its outcome
A verification is a relying party’s presentation request. The presentation
is verified and the verdict recorded, and nobody is signed in. Taken from
sts_oid4vp_wallet.js, section 7.
Console: on the Verifier request tab, under Start a verification,
choose the Format (dc+sd-jwt), Request (by reference), and
optionally the Client Identifier Prefix, Response type, Response
mode and Device, then click Start. The request appears under Open
verifications, and its Outcome changes from waiting for the wallet to
accepted or refused.
API (needs admin:write to start, admin:read to read):
api verifications/start '{"by":"reference","format":"dc+sd-jwt",
"redirect_uri":"https://verifier.example/verified"}'
# → state, walletUrl (same device), requestUri (openid4vp:// for a QR code),
# request_uri, dcql_query, expiresAt, …
curl -sk "$BASE/admin-api/verifications?state=$STATE" -H "Authorization: Bearer $TOKEN"
# → found: true, received, verdict {ok, checks, claims, …}
redirect_uri is your own page, where the Response Endpoint sends the wallet
once it has answered. In product mode it must be one of the calling client’s
registered redirect URIs, so the job leaves it out there; a start without one
gets {} back from /oid4vp/response. The other body members are by
(value or reference), client_id_prefix, response_type (vp_token,
id_token, vp_token id_token), response_mode (direct_post,
direct_post.jwt, form_post), mode (same-device or cross-device) and
wallet. GET /admin-api/verifications without state lists the open
verifications.
Settings: oid4vp.presentationRequestTtlS (600 s) is how long a request
waits for a wallet, and oid4vp.kbMaxAgeS (600 s) how old a Key Binding JWT
may be. oid4vp.expectedVct is the vct a presented SD-JWT VC must carry.
How a signed request names the Verifier
A request by value uses the redirect_uri: Client Identifier. A signed
request, by reference, uses the prefix oid4vp.clientIdPrefix names
(default pre-registered, with oid4vp.clientId, default sts-verifier),
or the client_id_prefix a start names. A wallet sign-in has its own setting,
oid4vp.signInClientIdPrefix, below.
x509_san_dns and x509_hash
Taken from sts_oid4vp_x509.js. The Verifier’s certificate is issued from
the realm’s CA the first time a request needs it.
- Read what a wallet must be configured with:
curl -sk "$BASE/oid4vp/verifier-certificate" # → response_uri, x509_san_dns {client_id, dns_name, …}, x509_hash {client_id, …}, # trust_anchor_pem - In product mode,
x509_san_dnsis refused (x509_san_dnsisnull) until a name is configured, unlessglobal.publicBaseUrlalready names the host. Set the name to the Response URI’s host:api config/set '{"key":"oid4vp.x509DnsName","value":"sts.example.com"}'A name that is not the Response URI’s host builds no
x509_san_dnsrequest.x509_hashneeds no name. - Start a request under either prefix:
api verifications/start '{"client_id_prefix":"x509_hash"}'or make it the realm’s default:
api config/set '{"key":"oid4vp.clientIdPrefix","value":"x509_hash"}'
The wallet must trust the service Root (trust_anchor_pem). The
x509_hash value changes whenever the certificate does: on renewal, on key
rotation, or when oid4vp.x509SigningAlgorithm (default ES256) changes.
The other prefixes
siop.js sets each of these, and each needs only the setting:
oid4vp.clientIdPrefix |
What else it needs |
|---|---|
decentralized_identifier |
nothing: the realm’s did:web and its DID document |
verifier_attestation |
oid4vp.verifierAttestation: a Verifier Attestation JWT an attestation issuer signed. Empty, the realm attests itself, which only a wallet that already trusts it accepts. |
openid_federation |
nothing: the realm’s Entity Configuration; see OpenID Federation |
Trust another issuer, and the status rules
By default the Verifier trusts only this realm’s own issuer. Taken from
sts_oid4vp_status_reference.js.
Console: on the OpenID4VP tab, paste the issuer’s PEM certificate into
oid4vp.trustedIssuerCertificates, and click Save OID4VP.
API:
api config/set '{"key":"oid4vp.trustedIssuerCertificates",
"value":"-----BEGIN CERTIFICATE-----\n…\n-----END CERTIFICATE-----"}'
Every presented credential must name its status by default
(oid4vp.requireStatusReference = all, in both modes). A trusted issuer
that publishes no status can be exempted by the SHA-256 thumbprint of its
certificate (hex, colon-hex as openssl x509 -fingerprint -sha256 prints it,
or base64url):
api config/set '{"key":"oid4vp.statusOptionalIssuers","value":"<sha-256 thumbprint>"}'
# or exempt every foreign issuer (warning: a credential its issuer revoked is then accepted)
api config/set '{"key":"oid4vp.requireStatusReference","value":"own-only"}'
off also accepts this realm’s own credentials without a status; it is
refused in product mode (STS-CORE-0103). A foreign issuer’s status list is
fetched, verified against the certificate that verified the credential, and
kept no longer than oid4vp.statusListMaxCacheS (3600 s).
Sign in with a wallet
The sign-in screen offers Sign in with a wallet to every request that
reaches it: an OAuth authorization request, a SAML AuthnRequest, a
wsignin1.0, the console and the portal. It is on by default
(oid4vp.signIn). Taken from sts_oid4vp_wallet.js, which runs it against a
running service in either mode and changes no setting.
What has to be true first:
- The person holds a credential this realm issued, on an access token
this realm verified: from
/portal/wallet, or by the authorization code flow (see Configuring OpenID4VCI). A credential from another realm, a trusted partner or a foreign token verifies and signs nobody in. - The wallet trusts this service’s Root. A sign-in’s request is signed
under the
x509_hashClient Identifier by default and carries the Verifier’s chain inx5c.GET /oid4vp/verifier-certificateshows the anchor (trust_anchor_pem). - The relying party is registered as for any sign-in. The job registers
a public OpenID Connect client,
sts-oid4vp-wallet-job-rp, with redirect URIhttps://rp.sts-oid4vp-wallet-job.example.test/cband scopeopenid; see Configuring OpenID Connect flows.
What the person does: on the sign-in screen, Sign in with a wallet,
then Use a wallet on this or a nearby device (the Digital Credentials
API) or Open your wallet on this device. The wallet shows what is asked
for, answers, and the browser continues the waiting request. The session says
amr ["pop"].
Settings (on the OpenID4VP tab). The response mode and Client Identifier defaults are what HAIP 1.0 has a Verifier send; change them only for a wallet that needs it:
| Setting | Default | Why you would change it |
|---|---|---|
oid4vp.signIn |
true |
false removes the wallet from the screen; /authn/wallet then answers 403. |
oid4vp.signInFormats |
dc+sd-jwt,jwt_vc_json,ldp_vc |
The formats asked for, in order. oid4vp_sign_in_formats.js narrows it to jwt_vc_json. |
oid4vp.signInResponseMode |
direct_post.jwt |
direct_post for a wallet that cannot encrypt. |
oid4vp.signInClientIdPrefix |
x509_hash |
pre-registered for a wallet that knows oid4vp.clientId out of band. |
oid4vp.signInDcApiResponseMode |
dc_api.jwt |
dc_api for a Digital Credentials API answer in the clear. |
oid4vp.responseEncryptionKeyAlgs |
HPKE-10-KE,ECDH-ES |
The keys an encrypted answer may be encrypted to. Leaving out ECDH-ES refuses every wallet that has only it. |
oid4vp.walletInvocation |
openid4vp |
haip-vp hands the request over under haip-vp://. |
oid4vp.signInCrossDevice |
false |
true offers Show a QR code instead, for a wallet on another device. A QR code can be relayed to a victim, so it is off. |
oid4vp.signInTtlS |
300 |
How long a sign-in waits for the wallet and the browser. |
oid4vp.signInPollS |
3 |
How often the QR-code page checks. |
oid4vp_sign_in.js turns the wallet sign-in off, turns the QR code on, and
sets these together for a wallet that answers in the clear under a
pre-registered identifier:
api config/set-many '{"oid4vp.walletInvocation":"haip-vp",
"oid4vp.signInResponseMode":"direct_post",
"oid4vp.signInClientIdPrefix":"pre-registered"}'
Disowning a credential. A credential stops signing in once it is
suspended or revoked on Monitoring → Verifiable Credentials
(/admin/vc/monitor), revoked on Tokens
(/admin/tokens), or after an administrator’s global sign-out. An ordinary
sign-out of one application disowns nothing.
A wallet as one of two factors
Taken from oid4vp_wallet_mfa.js. Nothing extra needs configuring:
- When the request, the realm’s authentication policy or the account demands
two factors, the person is asked for a second factor after the wallet
(their authenticator app, their security key, or their password), and the
session says, for example,
amr ["pop","otp"]andacr "mfa". - After a password, every second-factor screen links to Use your wallet
instead. Only the same person’s credential is accepted (
STS-VC-0084). - A key attestation the issuer verified can make the wallet two factors on its own; see Configuring OpenID4VCI → Key proofs.
Send an application’s people straight to the wallet
The named authentication
policy an application
names in appAuthnPolicy lists the mechanisms its people may sign in with.
With wallet the only one listed, a sign-in to that application goes
straight to /authn/wallet. Taken from oid4vp_wallet_mfa.js.
Console: on Directory → Policies, tab Authentication policy,
sub-tab New policy, create a named policy (wallet-only, say) with only wallet ticked under
the first-factor mechanisms. Then on the application’s Configuration
tab, Every protocol sub-tab, choose it in appAuthnPolicy and click
Save Every protocol.
API:
api policies/save-named-authn-policy '{"name":"wallet-only","mechanisms":["wallet"],"secondFactors":[]}'
api applications/set '{"application":"wsi-wallet","attribute":"appAuthnPolicy","value":"wallet-only"}'
While oid4vp.signIn is off, the screen says the wallet is switched off
instead.
Offer a web wallet for presentations
By default a request is handed to a wallet app under openid4vp://. A
web wallet is offered beside it, as Open your web wallet, only where one
is configured. Taken from oid4vp_sign_in.js:
api config/set-many '{"oid4vp.walletUrl":"https://web-wallet.example/w",
"oid4vp.walletPresentationPath":"/present"}'
The link is then https://web-wallet.example/w/present?client_id=…&request_uri=….
oid4vp.walletUrl falls back to oid4vci.walletUrl, so a web wallet set for
issuance is offered here too. For the embedded debugger’s web wallet the path
is /vc-presentation-1.html. In product mode a verification start may name
another wallet only if it is in oid4vp.allowedWalletUrls.
The eudi-dev hand run in tests/eudi-wallet-poc/README.md (a hand run, not a
test job; see Configuring OpenID4VCI)
signs in this way: it chooses Sign in with a wallet on the screen,
follows Open your web wallet to the wallet on port 8085, and approves the
wallet’s consent dialog.
Self-issued ID Tokens (SIOPv2)
A wallet can sign a person in with an ID Token it signs with its own key.
Nobody vouches for such a key, so it signs in only the person it was enrolled
for, in both modes. Taken from sts_siop.js.
1. Turn it on (off by default):
Console: on the OpenID4VP tab, set oid4vp.signInSelfIssued and click
Save OID4VP.
API:
api config/set '{"key":"oid4vp.signInSelfIssued","value":true}'
The sign-in screen then offers Sign in with a self-issued ID, and the
portal’s /portal/self-issued page lets a person enrol a key.
2. Enrol a key, one of two ways:
- The person, on
/portal/self-issued: Enrol a wallet. The wallet answers a SIOPv2 request from the session the person already holds, and the key that signed it is enrolled — adid:webtoo, whose document is fetched once for that enrolment and whose key is pinned. Nothing is typed. The row says Proved with your wallet, and the enrolment is audited (portal.siop.enrol). Its Location says where the key came from: the web wallet’s host (its configuredoid4vp.walletUrlon hover), the name a wallet app reported in itsUser-Agent, or the browser’s Digital Credentials API. Each row is one line; hover over a value for all of it. - An administrator: on the person’s page under Directory → Users,
section Self-issued IDs, enter the Subject (a
did:jwk, adid:web, or the JWK thumbprint) and a Label, then click Enrol. The row’s How says By value, unproven. Adid:web’s document is fetched as you enrol it and its authentication keys are pinned; if it cannot be fetched the enrolment is refused. Through the API:
api users/enrol-self-issued-subject '{"user":"<username>",
"subject":"<base64url JWK thumbprint>","label":"job wallet"}'
curl -sk "$BASE/admin-api/users/self-issued-subjects?user=<username>" -H "Authorization: Bearer $TOKEN"
# → subjects listed as urn:ietf:params:oauth:jwk-thumbprint:sha-256:<thumbprint>
api users/remove-self-issued-subject '{"user":"<username>","subject":"<thumbprint>"}'
Enrolling the same key twice is refused. A person may hold ten, and a subject
belongs to one person per realm. A removed key signs nobody in. Each listed
subject carries proof (wallet or admin), and a did:web its
pinnedKeys.
If a did:web wallet’s key changed, its sign-ins are refused
(STS-VC-0126) until the person clicks Enrol a wallet again and proves
the DID with the new key, which re-pins it. A did:web enrolled before
pinning is pinned on its next sign-in in development mode and refused
(STS-VC-0127) in product mode until it is enrolled again the same way.
3. Without signing anybody in, a verification can exercise a Self-Issued OP:
api verifications/start '{"response_type":"id_token","response_mode":"form_post"}'
oid4vp.siopIdTokenMaxAgeS (300 s) is how old the ID Token’s iat may be.
DIDs
The realm’s own DID
Each realm has a did:web, derived from the address the service is reached
at. Nothing needs configuring. The document is at /.well-known/did.json
(for a realm, /realm/<id>/did.json), and the Domain Linkage Credential at
/.well-known/did-configuration.json. Set global.publicBaseUrl so the DID
does not change with the host name. The issuer configurations that use it are
in Configuring OpenID4VCI → An issuer named by a DID.
An application’s DID
An application declared for the DID family gets
did:web:<host>[:realm:<id>]:applications:<identifier>, with its document at
/applications/<identifier>/did.json. Taken from application_did.js.
Console:
- Create the application with Decentralized Identifier (DID) ticked under Protocol families it is declared for, or add the family to an existing one.
- On its Credentials tab, under DID key pair, choose an algorithm after Generate a key pair: (ES256, ES384 or EdDSA) and click Generate. The private key is shown once, and a sealed copy is kept so the service can sign the domain linkage. Tick replace the keys there to make the new key the only one.
- Add a
LinkedDomainsservice todidService, for exampleLinkedDomains|https://app.example.com, with Add to in Change what it is allowed to do. - Under Domain linkage, click Download did-configuration.json and
host the file at
https://app.example.com/.well-known/did-configuration.json.
API:
api applications/create '{"identifier":"ad-app","name":"ad-app","protocols":["did"],"fields":{}}'
api applications/generate-did-key '{"application":"ad-app","algorithm":"ES384","replace":true}'
api applications/add '{"application":"ad-app","attribute":"didService",
"value":"LinkedDomains|https://app.example.com"}'
api applications/add '{"application":"ad-app","attribute":"didAlsoKnownAs",
"value":"https://app.example.com"}'
api applications/sign-domain-linkage '{"application":"ad-app","origin":"https://app.example.com"}'
# → didConfiguration, to host at hostAt
A key you hold yourself can be added to didPublicKeyJwk as a public JWK
instead, but only a key generate-did-key made can sign the domain linkage
(STS-VC-0115). The Domain Linkage Credential is good for
oid4vci.domainLinkageLifetimeS (a year by default).
Related
- OpenID4VP, wallet sign-in and DIDs: the behaviour and every setting.
- Configuring OpenID4VCI: issuing what a wallet presents.
- Authentication, for the sign-in screen and second factors.
- OpenID Federation, for the
openid_federationprefix. - Configuring OpenID Connect flows and Configuring OAuth 2.0 grants.