Verifying Credentials
O2ID can act as an OID4VP
verifier: a verifiable_credential_presentation authenticator
asks the subject to present a credential from their wallet, checks its
signature and issuer against an explicit trust list, and feeds the
disclosed claims into the rest of the flow — "log in by presenting your
employer's badge," or a step-up check partway through an
already-authenticated flow. It's configured and referenced exactly like
TOTP: create an authenticator instance, then
point an authenticator flow node at its ID. There is no separate node
type for this.
This is the mirror image of issuing credentials: O2ID is the relying party here, not the issuer. The credential being presented was typically issued by some other system (possibly another O2ID tenant, possibly an entirely external issuer) — see Trusting an issuer below for what "external" requires.
Configuring the authenticator
o2idctl authenticators create \
--type verifiable_credential_presentation \
--display-name "Employee Badge Presentation" \
--field vct=EmployeeBadge \
--field trustedIssuers=https://partner.example.com/t/system \
--field requiredClaims=department
| Field | Required | Description |
|---|---|---|
vct | Yes | The credential type the wallet must present — checked against the presented credential's own vct claim |
trustedIssuers | Yes | Comma-separated allow-list of issuer URLs. There is no "accept any issuer whose JWKS resolves" mode — an issuer not on this list is rejected before any network request is made to it |
requiredClaims | No | Comma-separated claim names that must be present (after selective disclosure) for the presentation to succeed |
Then reference it from a flow the same way as any other authenticator:
{
"type": "authenticator",
"onSuccess": "allow",
"onFailure": "deny",
"properties": {
"authenticatorId": "<verifiable_credential_presentation-authenticator-id>",
"maxAttempts": 3,
"onLockout": "deny"
}
}
maxAttempts/onLockout work exactly like they do for TOTP: each failed
presentation counts as an attempt, and hitting the limit routes to
onLockout instead of re-issuing another challenge.
What happens at runtime
- The node pauses immediately (no fields to submit) — the client
receives a
nodeViewof typeauthenticatorwhosedata.requestUriis anopenid4vp://deep link/QR code. - The wallet (often a different device than the one that scanned the
code) presents the credential by POSTing directly to
POST /oid4vp/response— unauthenticated, gated by the single-use challenge ID embedded in the request asstate. - O2ID verifies the credential (see below) and resumes the flow past
the
authenticatornode, routing toonSuccessoronFailure. - Because step 2 may happen on a different device, the portal polls
GET /flow-runs/{id}until the run advances, rather than waiting for a redirect back to the same browser tab (contrast with aconnectionnode's federation redirect).
Trusting an issuer
There is no "accept any issuer whose JWKS resolves" mode. A presented
credential's own iss claim is checked against trustedIssuers before
any network request is made to it — an issuer not on the list is
rejected outright. For a trusted issuer, O2ID discovers its jwks_uri via
{issuer}/.well-known/openid-configuration and verifies the credential's
signature against it — any issuer that publishes standard OIDC discovery
works, not only ones that happen to share O2ID's own JWKS path convention.
Holder-binding replay protection
Each presentation request carries a freshly minted nonce (distinct from
the state used to correlate the response with the pending flow run). When
the presented credential was issued with a cnf key (see
Issuing Verifiable Credentials), O2ID
requires and verifies a Key-Binding JWT (KB-JWT) at the end of the
presentation: it must be signed by that cnf key and its own nonce claim
must match the nonce O2ID generated for this exact request. A captured
presentation replayed against a different request — even one for the same
flow, moments later — fails, because it carries the wrong nonce. A
credential issued without a cnf key (bearer-only) has no key to bind to,
so no KB-JWT is required or checked for it.
Known limitations
- SD-JWT VC only. JWT-VC and mdoc presentation verification aren't implemented yet (mdoc issuance is supported — see Managing Verifiable Credentials — but presenting one back for verification is not).
- Unsigned authorization requests. The
openid4vp://request is sent by value, not as a signed request object. - No
audbinding check on the KB-JWT. Only thenonceis verified; the KB-JWT'saudclaim (if the wallet sets one) isn't checked against a specific verifier identifier.
API Reference
See the API Reference for POST /oid4vp/response's request/response shape.