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.
- 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.
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
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.
Step 2: Display the QR code or deep link
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:trueif the wallet proved possession of the private key bound to the credential. Reject the login if this isfalse.kbKeyId: an RFC 7638 JWK Thumbprint of the holder's public key. The same wallet with the same credential always produces the samekbKeyId. 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.
- cURL
# 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
kbSignatureIsValidandkbKeyIdin theFULFILLEDpayload - The
kbKeyIdis 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
responseCodefor 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
- KYC verification—verify customer identity using PID credentials
- Error handling—build robust status handling for all verification outcomes
- Going to production—production readiness checklist
Further reading
- Key binding explained—how holder binding works in the EUDI ecosystem
- Authentication use case—conceptual overview of passwordless authentication
- Error codes—wallet-facing HTTP error responses
- Callback events—Presented Credentials Event statuses and payload fields