Discuss a project
menu

OpenID4VP replay protection: binding proof to the right session

How to scope an OpenID4VP verifier integration around replay protection, session binding, concurrent callbacks and a concrete acceptance test pack.

An OpenID4VP verifier needs to establish that a presentation belongs to the intended verifier and request, then associate the accepted result with the correct application session. A valid issuer signature alone does not answer those questions. This is one of the boundaries worth reviewing before a wallet demonstration becomes a customer-facing integration.

Consider a fictional membership platform adding wallet-based identity verification to an existing account area. A customer starts a check in a browser, approves it on a phone and returns to the account page. The team can demonstrate the successful path. The next engineering milestone is to prove that an old response, a second browser or two simultaneous callbacks cannot change the wrong account.

This article proposes the project scope and acceptance evidence for that milestone. It is not a report of a client engagement or a complete verifier implementation. Protocol references were checked against the final OpenID for Verifiable Presentations 1.0 specification, published 9 July 2025.

separate the protocol check from the account update

For presentations with holder binding, OpenID4VP requires proof bound to the verifier's client identifier and the request nonce. The verifier checks that binding for each presentation. The specification also requires credential-format validation, satisfaction of the requested query and applicable verifier-policy checks. Presentations intentionally requested without holder binding do not provide the same replay protection. See sections 8.6 and 14.1.

In our proposed platform, these checks produce an internal verification result. A separate application operation decides whether that result can update the membership account that initiated the flow.

Keep both operations explicit in the design. If a vendor supplies the verifier, ask which checks it performs, which identifiers it returns and what evidence the application must validate before trusting the callback. A field named verified: true leaves too much unspecified.

define the transaction before displaying the wallet request

Create a server-side transaction associated with the authenticated account and browser session. For this example, the transaction record would include:

Internal transaction ID
Initiating account and session reference
Expected verifier identity and request nonce
Requested credential profile and claims
Acceptance-policy version
Created time and expiry
Lifecycle state and revision

These are proposed application fields, not an OpenID4VP wire payload. Do not send the internal account reference to the wallet merely because it is convenient to serialize the record.

Generate public correlation values independently from private session credentials. Treat callback values as untrusted inputs and validate them against the stored transaction. Use the framework's session controls and server-side expiry enforcement; OWASP's session-management guidance covers unpredictable identifiers, protected cookies and session lifecycle handling.

The product owner should decide what happens when the customer signs out, switches accounts or begins another verification. Our proposed first release cancels the pending attempt on sign-out or account switch, and gives a replacement attempt a new transaction. That keeps the expected behaviour testable.

make callback acceptance a single state transition

A frequent integration mistake is to read pending, perform validation, and later write accepted without checking whether another request changed the transaction in between. Two workers can both observe the original state.

Our proposed design uses conditional persistence when accepting a fully validated result. The operation checks that the transaction is still pending, unexpired and associated with the expected policy revision. Only the operation that successfully changes that state may schedule the account update.

pending -> accepted
pending -> rejected
pending -> expired
pending -> cancelled

accepted/rejected/expired/cancelled -> no new acceptance

Use a database transaction or the equivalent atomic mechanism in the selected store. If the membership update happens in another service, give it an idempotency key tied to the internal transaction and persist its delivery obligation. Otherwise a crash between accepting the proof and notifying the account service creates a result that nobody can finish safely.

An invalid callback should not automatically consume a valid customer's pending attempt. Decide which failures leave the transaction pending, which terminate it and which trigger rate limiting. That choice belongs in the threat model and tests, rather than emerging accidentally from exception handling.

For repeated delivery of an already accepted callback, return a bounded acknowledgement if the integration requires it. Do not perform the membership update twice, disclose the previous identity payload, or allow a different response to overwrite the accepted result.

give the browser a protected way to read the result

In the proposed account flow, the result endpoint checks the current session and account against the transaction before returning a status. Knowing a transaction reference is not sufficient to read identity attributes or claim the result.

The browser should never declare itself verified because it reached a success URL. It asks the backend, and the backend answers from the accepted transaction. A support-facing operation may need different access rules, but it should use an explicit authorization path rather than the customer's correlation value.

OpenID4VP discusses session fixation and the particular risks of cross-device flows in section 14.2. Associating a transaction with its initiating browser does not by itself prevent a user being tricked into approving a request created by an attacker. Evaluate the selected wallet invocation method, request presentation and applicable anti-phishing protections separately.

That distinction is especially relevant when adapting a membership check into a login or recovery mechanism. Do not assume that a flow approved for updating an existing account is also an adequate way to create an authenticated session.

commission the failure cases alongside the successful flow

The useful deliverable is a reproducible acceptance pack using synthetic identities. For the membership platform, we would scope these cases before implementation:

  1. Old response, new attempt. Start a second attempt and submit the response captured for the first. Confirm that it cannot satisfy the second request.
  2. Different verifier. Present evidence produced for a different client identity. Confirm rejection before the account service sees a success event.
  3. Concurrent callbacks. Deliver the same valid response to two workers at once. Confirm one acceptance transition and one account effect.
  4. Wrong browser or account. Request the result from another session, and switch accounts during the pending flow. Confirm the documented access and cancellation behaviour.
  5. Expired attempt. Submit otherwise acceptable evidence after the application transaction expires. Confirm the user can start a new attempt without reviving the old one.
  6. Invalid callback first. Send malformed input before the legitimate response. Verify the chosen failure policy and that retries cannot bypass validation.
  7. Crash after acceptance. Stop processing before the account update, then recover. Confirm the durable update operation completes once.
  8. Incomplete evidence. Return an otherwise authentic credential that lacks a required claim. Confirm the application does not infer the missing value.

Capture transaction states and bounded outcomes in the test report. Avoid using raw credentials, session secrets or customer attributes as debugging identifiers. A reviewer should be able to trace the test without receiving a second identity database in the logs.

define what the client receives at handover

Ask for a versioned integration contract that names the credential formats, wallet profiles, verifier component and configuration actually tested. Include the state model, callback authentication boundary, test fixtures, deployment configuration and recovery runbook in systems the client controls.

The runbook should explain how to distinguish a user cancellation from a failed credential check, an expired request and a delivery failure. Name the owner of issuer-trust policy and the owner of application account matching. Those responsibilities often sit with different teams.

A good first scope is one credential profile, one existing account journey and the failure cases above. Expand after that boundary is demonstrated. For the surrounding architecture, see integrating an EUDI wallet with an existing customer platform. For the issuer side, see turning enterprise records into credential issuance.

Our identity engineering practice can help scope the verifier, application and operational work together. Bring the account flow, intended wallet profile and current identity components, and let's define the integration and its acceptance tests.

Working on something similar? Talk to us

← all writing