Skip to main content

Implement passwordless authentication

This guide shows you how to implement passwordless authentication using key binding proof from an EUDI Wallet. You create a presentation request that enforces cryptographic holder binding, verify the proof in your callback, and extract a stable user identifier to bind to sessions or accounts.

For a step-by-step learning experience that builds a complete project from scratch, see the passwordless authentication tutorial. For a conceptual overview, see Authentication with EUDI Wallet.

Prerequisites
  • A running connector instance with its public base URL configured and a callback URL pointing to your backend endpoint (for example, http://localhost:3000/callback/auth). See connector architecture for the deployment model and how callbacks are delivered.
  • Access to the internal management API (port 8081)
  • A callback endpoint reachable from the connector over internal networking
  • An X.509 access certificate configured in the connector
  • An Account Ownership Credential (AOC) already issued to the user's wallet

Overview

You build an authentication flow where users prove possession of a credential bound to their wallet's private key. You set require_cryptographic_holder_binding to true in the presentation request, which instructs the connector to request a Key Binding JWT (KB-JWT)—a signed JWT proving the presenter controls the credential's private key—from the wallet. The callback then includes kbSignatureIsValid and kbKeyId—an RFC 7638 thumbprint of the holder's public key—that you use as a stable, privacy-preserving identifier to recognize returning users.

Time to implement: 1–2 hours.

Step 1: Create a presentation request with key binding

Call POST /oidc4vp with require_cryptographic_holder_binding: true. This parameter is not part of the DCQL specification—it is a connector API parameter that controls whether the connector requests a KB-JWT from the wallet.

note

The VCT values, requested claims, and credential values in this guide are examples. In production, use the VCT and claims defined by your authentication credential type. The key binding pattern is the same regardless of credential type.

curl -X POST http://connector:8081/oidc4vp \
-H "Content-Type: application/json" \
-d '{
"dcql_query": {
"credentials": [
{
"id": "aoc",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["urn:eudi:aoc:1"]
},
"claims": [
{ "path": ["account_id"] }
],
"require_cryptographic_holder_binding": true
}
]
}
}'

Example response:

{
"state": "abc123",
"same_device_request_uri": "openid4vp://?client_id=...&request_uri=https%3A%2F%2Fconnector.example.com%2Foidc4vp%2Fabc123%2Frequest%3Fflow_type%3Dsame-device",
"cross_device_request_uri": "openid4vp://?client_id=...&request_uri=https%3A%2F%2Fconnector.example.com%2Foidc4vp%2Fabc123%2Frequest%3Fflow_type%3Dcross-device"
}

Store the state value to correlate with the callback later.

The format remains dc+sd-jwt—key binding is controlled by the require_cryptographic_holder_binding parameter, not by the format string.

Use cross_device_request_uri for QR codes and same_device_request_uri for deep links, the same way as in the KYC verification guide.

Step 3: Implement the callback handler

Implement a callback endpoint that receives the PresentedCredentialsEvent from the connector. The state field correlates the event with your original request. For same-device flows, the responseCode field correlates the browser redirect with the callback.

The authentication callback includes two fields beyond a standard credential verification:

  • kbSignatureIsValid: true if the wallet proved possession of the private key bound to the credential. Reject the login if this is false.
  • kbKeyId: an RFC 7638 JWK Thumbprint of the holder's public key. The same wallet with the same credential always produces the same kbKeyId. Use it as a stable user identifier.

The credentials and credentialsRaw fields are absent for REJECTED, EXPIRED, PROCESSING_ERROR, and VERIFICATION_FAILED statuses. The errorDetails field is present for REJECTED, PROCESSING_ERROR, and VERIFICATION_FAILED statuses.

# Simulate a FULFILLED callback with key binding for testing
curl -X POST http://localhost:3000/callback/auth \
-H "Content-Type: application/json" \
-d '{
"status": "FULFILLED",
"state": "abc123",
"responseCode": "iaBjmfYXonNRo6MdqLQEsA",
"credentials": {
"aoc": [
{
"issuer": "https://issuer.example.com",
"claims": {
"account_id": "04171643"
},
"signatureIsValid": true,
"supportRevocation": false,
"supportTrustAnchor": true,
"isTrusted": false,
"kbSignatureIsValid": true,
"kbKeyId": "KrXxHnYsf-Inp0hW1M6r32JfOZfEGX7QTr7wa-bnXlM"
}
]
},
"credentialsRaw": {
"aoc": [
{
"claims": "eyJhY2NvdW50X2lkIjoiMDQxNzE2NDMifQ==",
"issuer": "https://issuer.example.com",
"kbKeyId": "KrXxHnYsf-Inp0hW1M6r32JfOZfEGX7QTr7wa-bnXlM"
}
]
}
}'

For the complete list of statuses and error codes, see the callback events reference.

Testing

Test checklist

  • Presentation request creates with require_cryptographic_holder_binding: true
  • Callback receives kbSignatureIsValid and kbKeyId in the FULFILLED payload
  • The kbKeyId is stable across multiple authentication attempts from the same wallet
  • Failed key binding proof (kbSignatureIsValid: false) is rejected
  • Callback handles all five statuses (FULFILLED, REJECTED, EXPIRED, PROCESSING_ERROR, VERIFICATION_FAILED)
  • Same-device flow correctly uses responseCode for browser redirect correlation

Troubleshooting

Missing kbKeyId

Verify that require_cryptographic_holder_binding is not explicitly set to false in the request body. The field defaults to true when omitted, so the connector requests a key binding proof by default. If the field is set to false, the connector does not request a key binding proof, and kbKeyId and kbSignatureIsValid are absent from the callback payload.

Callback not receiving events

The connector delivers callbacks synchronously with a default two-second timeout per attempt and one retry. Verify that your callback endpoint is reachable from the connector and responds within the timeout.

Unstable user identifier

The kbKeyId is derived from the holder's public key using RFC 7638. It remains stable as long as the user presents the same credential from the same wallet. If the user re-provisions their wallet or obtains a new credential, the kbKeyId changes.

Next steps

Further reading