Skip to content

Example flow — High-assurance issuance with HAIP

Audience: Developers new to the EUDI Wallet Ecosystem who need to understand how high-assurance credential issuance works in practice.


Overview

This document walks through a complete high-assurance credential issuance flow using the HAIP (High Assurance Interoperability Profile) pattern, built on OpenID4VCI. HAIP is designed for credentials that require strong identification and security guarantees — professional licenses, health credentials, government-issued attestations, or other legally significant credentials.

This guide covers what HAIP is and when to use it, how the Authorization Code flow works in the context of OpenID4VCI, the complete technical journey from user initiation to credential delivery, and key security considerations.


What is HAIP?

HAIP (High Assurance Interoperability Profile) is a security profile for OpenID4VCI that specifies:

  • Strong holder binding using DPoP (Demonstrating Proof of Possession) — tokens are bound to a specific key held by the wallet
  • Sender-constrained access tokens that cannot be replayed by an attacker who intercepts them
  • High-assurance identification, typically using PID (Person Identification Data) or an equivalent strong authentication process
  • Refresh token support for credential lifecycle management

Use HAIP when your credential has legal, regulatory, or safety implications and when your rulebook requires robust identification and strong security controls.


When to use this flow

Use a HAIP-style flow when:

  • Your credential requires high assurance (as defined in your rulebook)
  • Strong online identification is required at issuance time (e.g., PID-based identification)
  • Holder binding must be cryptographically strong
  • Long-term credential management is needed (refresh, revocation)
  • Your credential has legal or regulatory significance

Examples: professional licenses, medical credentials, education certificates, employment attestations with legal weight, municipal proof-of-residence (Meldebestätigung), social benefit entitlements, local government permits.


Complete flow walkthrough

Prerequisites

Before issuance can begin:

  • The EAA Provider has published issuer metadata at a well-known URL
  • The holder has a EUDI Wallet installed and operational
  • The EAA Provider has determined the holder is eligible for the credential
  • The provider has access to authoritative source data for credential attributes

Step 1: Holder identification and eligibility determination

The holder completes a high-assurance identification process as required by your credential's rulebook. This typically involves:

  • PID-based identification: The holder presents their PID (Person Identification Data) credential from their wallet to prove their identity
  • Alternative strong identification: Depending on your rulebook, this could be sector-specific authentication, eID, or another high-assurance method
  • Eligibility verification: The EAA Provider queries internal or external source systems to confirm the holder is eligible for the credential

PID presentation typically happens via OpenID4VP (Verifiable Presentations). The verifier (EAA Provider) receives authenticated attributes from the PID, maps them to an internal user record or eligibility database, and confirms eligibility (e.g., "this person holds a valid professional license").

Example (professional credential): A healthcare professional wants to obtain a digital medical license credential. They visit the licensing authority's portal, which requests their PID to verify their identity. The authority confirms they hold an active medical license in their database.

Example (government credential): A resident wants a digital proof-of-residence credential. They visit their municipality's portal (Einwohnermeldeamt), which requests their PID to verify their identity. The system confirms their registered address from the civil registry and triggers credential issuance.


Step 2: Credential offer generation

Once the holder is identified and eligible, the EAA Provider generates a credential offer and delivers it to the holder.

The credential offer is a JSON object that tells the wallet:

  • Which issuer is offering the credential
  • Which credential type(s) are available
  • How to obtain the credential (grant type and parameters)

Example credential offer (Authorization Code flow):

{
  "credential_issuer": "https://medical-board.example",
  "credential_configuration_ids": ["medical_license_sd_jwt"],
  "grants": {
    "authorization_code": {
      "issuer_state": "eyJhbGc...state_token"
    }
  }
}

Delivery methods: - QR code displayed on a web portal (cross-device flow) - Deep link on the same device (same-device flow)

The credential offer is encoded and presented to the holder. When scanned or clicked, it triggers the wallet to begin the issuance flow.


Step 3: Wallet initiates authorization

The wallet receives the credential offer and begins the Authorization Code flow by redirecting the holder to the issuer's authorization endpoint:

GET /authorize?
  response_type=code&
  client_id=wallet.example&
  redirect_uri=https://wallet.example/callback&
  state=wallet_state_abc123&
  code_challenge=sha256_challenge&
  code_challenge_method=S256&
  scope=openid credential&
  issuer_state=eyJhbGc...state_token
  HTTP/1.1
Host: as.medical-board.example
  • code_challenge: PKCE challenge — prevents an attacker from redeeming a stolen authorization code
  • issuer_state: Links this authorization request to the specific credential offer generated in Step 2
  • redirect_uri: Where the authorization server returns the result

The wallet redirects the holder to the authorization server's authentication page — typically a web page or in-app authentication flow.


The holder authenticates at the authorization server and consents to credential issuance. Authentication uses a strong method per the rulebook (eID, username/password + MFA, or re-using recent PID identification). The authorization server binds this authentication session to the authorization request parameters. The holder is then shown what will be issued and explicitly consents:

Medical License Board Authorization

You are about to receive:
- Medical License Credential
- Contains: Name, License Number, Specialization, Expiry Date

This credential will be issued to your EUDI Wallet.

[Approve] [Cancel]

In high-assurance flows, the authorization server must verify that the authenticated identity matches the eligibility determination from Step 1, that authentication strength meets the rulebook's requirements, and log the consent event for audit purposes.


Step 5: Authorization code issuance

After successful authentication and consent, the authorization server generates an authorization code and redirects the holder back to the wallet (OID4VCI §3.4):

HTTP/1.1 302 Found
Location: https://wallet.example/callback?
  code=auth_code_xyz789&
  state=wallet_state_abc123
  • code: The authorization code — short-lived (typically 60–300 seconds), single-use, and PKCE-bound
  • state: The wallet's state parameter, returned unchanged so the wallet can validate the response

Step 6: Token exchange

The wallet exchanges the authorization code for an access token and refresh token at the token endpoint (OID4VCI §6, RFC 9449 DPoP):

POST /token HTTP/1.1
Host: as.medical-board.example
Content-Type: application/x-www-form-urlencoded
DPoP: eyJhbGc...dpop_proof_jwt

grant_type=authorization_code&
code=auth_code_xyz789&
redirect_uri=https://wallet.example/callback&
client_id=wallet.example&
code_verifier=original_pkce_verifier

DPoP proof

In HAIP, the wallet attaches a DPoP proof to the token request. A DPoP proof is a short-lived, request-bound JWT that the wallet signs with its private key. It proves the wallet controls that key at this specific endpoint, binding the resulting access token to the wallet's key — so even if the token were intercepted, it cannot be used without the private key.

The DPoP HTTP header contains this JWT in compact serialization (base64url(header).base64url(payload).signature). Below is the decoded structure before signing:

// DPoP proof — header
{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
// DPoP proof — payload
{
  "jti": "unique-request-id",
  "htm": "POST",
  "htu": "https://as.medical-board.example/token",
  "iat": 1234567890
}

htm and htu bind this proof to the specific HTTP method and URL, preventing replay at any other endpoint. After the wallet signs it, the three parts are base64url-encoded and concatenated with . separators to form the compact JWT value sent in the header.

Token response:

{
  "access_token": "eyJhbGc...access_token_jwt",
  "token_type": "DPoP",
  "expires_in": 86400,
  "refresh_token": "eyJhbGc...refresh_token_jwt",
  "c_nonce": "1a105ffb-0c70-4a83-b86b-8c803de3009d",
  "c_nonce_expires_in": 86400
}
  • access_token: DPoP-bound access token for credential issuance
  • token_type: "DPoP": Signals this is a sender-constrained token requiring a DPoP proof on each use
  • refresh_token: Long-lived token for credential refresh without re-authentication
  • c_nonce: Single-use nonce the wallet must include in its proof-of-possession at the credential endpoint

Security characteristics: - The access token is bound to the wallet's DPoP key - The refresh token allows future credential updates without re-authentication - The authorization code is immediately invalidated after exchange


Step 7: Credential request with proof-of-possession

Using the c_nonce received in the token response, the wallet constructs a proof-of-possession (OID4VCI Appendix F.1) and requests the credential from the credential endpoint (OID4VCI §8).

POST /credential HTTP/1.1
Host: issuer.medical-board.example
Authorization: DPoP eyJhbGc...access_token_jwt
DPoP: eyJhbGc...dpop_proof_for_credential_endpoint
Content-Type: application/json

{
  "credential_configuration_id": "medical_license_sd_jwt",
  "proof": {
    "proof_type": "jwt",
    "jwt": "eyJhbGc...credential_proof_jwt"
  }
}

Proof-of-possession JWT (OID4VCI Appendix F.1)

The wallet creates a signed JWT proving it controls the private key that will be bound to the credential. Below is the decoded structure before signing:

// Proof JWT — header
{
  "alg": "ES256",
  "typ": "openid4vci-proof+jwt",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
// Proof JWT — payload
{
  "iss": "wallet.example",
  "aud": "https://issuer.medical-board.example",
  "iat": 1234567890,
  "nonce": "1a105ffb-0c70-4a83-b86b-8c803de3009d"
}

The nonce ties this proof to this specific issuance request and prevents replay. The jwk in the header is the wallet's public key — the issuer will embed this key in the issued credential, creating the cryptographic binding between credential and holder.


Step 8: Credential issuance

The issuer validates all proofs and tokens, retrieves authoritative data, constructs the credential, signs it, and returns it to the wallet.

  1. Validate access token:
  2. Check signature and issuer
  3. Verify it's intended for this credential endpoint (audience)
  4. Check expiry
  5. Verify it's bound to the DPoP key in the request

  6. Validate DPoP proof:

  7. Check signature matches the public key in the access token binding
  8. Verify HTTP method and URL match the request
  9. Check freshness (iat timestamp)

  10. Validate credential proof:

  11. Check signature is valid
  12. Verify the nonce matches a c_nonce issued by the issuer (unused and unexpired)
  13. Verify audience matches the issuer
  14. Extract the public key that will be bound to the credential

  15. Retrieve and verify attributes:

  16. Query source systems for current, authoritative data
  17. Apply attribute verification rules from the rulebook
  18. Ensure the authenticated user matches the credential subject

  19. Construct the credential:

  20. Populate all required attributes from source data
  21. Include the holder's public key (from the credential proof)
  22. Add credential metadata (issuer, issuance date, expiry, status reference)
  23. Apply selective disclosure configuration (for SD-JWT)

  24. Sign the credential:

  25. Use the issuer's trusted signing key
  26. Apply the appropriate signature algorithm (e.g., ES256)
  27. Ensure the signing key is registered in the trust framework

Example credential response (SD-JWT VC):

{
  "credential": "eyJhbGc...signed_credential_jwt"
}

The credential itself is a signed JWT (for SD-JWT format) or a CBOR-encoded signed structure (for mDoc format). If the credential response also includes a c_nonce, the wallet uses it for any subsequent credential request.

SD-JWT credential structure (decoded for illustration):

Header:
{
  "alg": "ES256",
  "typ": "vc+sd-jwt",
  "kid": "issuer-pubkey-1"
}
Payload:
{
  "vct": "https://medical-board.example/credentials/medical-license/1.0",
  "iss": "https://issuer.medical-board.example",
  "iat": 1234567890,
  "exp": 1297987890,
  "cnf": {
    "jwk": { /* holder's public key */ }
  },
  "_sd": [ /* selective disclosure hashes */ ],
  "name": "Dr. Jane Smith",
  "license_number": "MD-123456",
  "specialization": "Cardiology",
  "status": {
    "status_list": {
      "uri": "https://medical-board.example/status/1",
      "idx": 42
    }
  }
}
  • vct: Verifiable credential type identifier
  • iss: Issuer identifier
  • exp: Expiry timestamp
  • cnf: Confirmation claim containing the holder's public key — this is what binds the credential to the wallet
  • _sd: Selective disclosure hashes — the holder can reveal individual claims at presentation without disclosing the rest
  • status: Revocation status reference

Step 9: Wallet stores the credential

The wallet receives the credential, validates it, and stores it securely. The wallet checks that the credential is signed by a trusted issuer (verified against the trust framework), that it is currently valid (not yet expired), that the public key in the cnf claim matches the wallet's own key, and that all required fields are present.

The credential and associated private key are stored in secure storage (hardware-backed where available). The refresh token is stored for future credential renewal.

The holder sees a confirmation:

✓ Credential Added

Medical License
Medical License Board

Valid until: 31 Dec 2026

[View Details]

Credential lifecycle: Refresh flow

One of the key benefits of HAIP is built-in support for credential refresh using the refresh token.

When to refresh

Credentials should be refreshed: - Before expiry (proactively) - When attributes have changed - When the issuer requests refresh (e.g., via push notification)

Refresh protocol

Step 1: Wallet initiates refresh

POST /token HTTP/1.1
Host: as.medical-board.example
Content-Type: application/x-www-form-urlencoded
DPoP: eyJhbGc...dpop_proof_jwt

grant_type=refresh_token&
refresh_token=eyJhbGc...refresh_token_jwt

Step 2: Authorization server validates and issues new tokens

The authorization server: - Validates the refresh token (signature, expiry, revocation status) - Checks the holder's eligibility is still valid - Issues a new access token and optionally a new refresh token

{
  "access_token": "eyJhbGc...new_access_token",
  "token_type": "DPoP",
  "expires_in": 86400,
  "refresh_token": "eyJhbGc...new_refresh_token"
}

Step 3: Wallet requests refreshed credential

The wallet follows the same credential request flow as in Steps 7–8: it uses the c_nonce from the new token response, builds the proof, and uses the new access token at the credential endpoint. The holder does not need to re-authenticate or provide identification again.


Security considerations

Token security

DPoP binding: - Access tokens are bound to a specific wallet key via DPoP - Even if an access token is intercepted, it cannot be used without the corresponding private key - This provides sender-constrained token security

Short-lived access tokens: - Access tokens should have short lifetimes (typically hours to 1 day) - Reduces window of exposure if compromised

Long-lived refresh tokens: - Refresh tokens can be valid for the credential's full lifecycle (months to years) - Must be stored securely and protected against theft - Should support revocation in case of compromise

Holder binding

Cryptographic binding: - The credential includes the holder's public key in the cnf claim - During presentation, the holder proves possession of the corresponding private key - Prevents credential theft and replay attacks

Key protection: - Private keys should be stored in hardware-backed secure storage - Keys should never leave the secure element - Use device attestation where available to prove key security

Identification assurance

PID-based identification: - Using PID for identification provides high assurance of holder identity - The issuer receives government-verified attributes - The issuer must still map PID attributes to internal records

Identification binding: - The authorization server must ensure the authenticated user matches the identified subject - The time gap between identification and issuance should be minimized - Consider re-verification if significant time has passed

Audit and compliance

Logging requirements: - Log all authorization requests, token exchanges, and credential issuances - Include sufficient detail for audit and incident response - Use pseudonymous identifiers to minimize privacy impact - Implement retention policies aligned with regulatory requirements

Non-repudiation: - Maintain evidence of holder consent for credential issuance - Log proof-of-possession validation results - Store credential issuance records for the required retention period


Common implementation challenges

Challenge 1: Refresh token lifecycle management

Problem: Long-lived refresh tokens require careful lifecycle management.

Solution: - Implement refresh token rotation (issue new refresh token on each refresh) - Provide holder-initiated revocation through a self-service portal - Monitor for suspicious refresh patterns (unusual frequency, location, etc.) - Plan for refresh token expiry and holder re-onboarding

Challenge 2: Attribute freshness at refresh

Problem: Source data may change between issuance and refresh.

Solution: - Always query source systems during refresh, not just at initial issuance - Handle cases where eligibility has changed (deny refresh, revoke old credential) - Implement change detection and notify holders when attributes update - Consider versioning credentials if significant changes occur


Implementation checklist

Before you start

  • Rulebook defines high-assurance requirements and identification method
  • Trust framework registration is complete (issuer certificate obtained)
  • Source systems are accessible and provide authoritative data
  • Decision made on PID or alternative identification mechanism

Authorization server

  • Supports Authorization Code flow with PKCE
  • Implements strong authentication per rulebook requirements
  • Provides user consent interface
  • Issues DPoP-bound tokens (if implementing full HAIP)
  • Supports refresh token grant
  • Logs authentication and authorization events

Token endpoint

  • Validates authorization codes (signature, expiry, PKCE, single-use)
  • Validates DPoP proofs
  • Issues access tokens bound to DPoP key
  • Issues refresh tokens with appropriate expiry
  • Implements rate limiting and monitoring

Credential endpoint

  • Validates access tokens (signature, audience, expiry, DPoP binding)
  • Validates credential proof-of-possession (signature, nonce, audience)
  • Queries source systems for fresh attribute data
  • Constructs credentials per technical schema
  • Includes holder's public key in credential (cnf claim)
  • Signs credentials with registered issuer key
  • Includes revocation status reference
  • Rejects proofs with missing, reused, or expired nonces

Metadata

  • Publishes credential issuer metadata at the .well-known endpoint (inserted-path form)
  • Declares supported credential configurations
  • Lists supported proof types and signing algorithms
  • Specifies the credential endpoint and references the authorization server(s) (token and authorization endpoints come from the AS metadata); nonce_endpoint is optional

Lifecycle management

  • Implements refresh token flow
  • Queries source systems during refresh
  • Handles attribute changes during refresh
  • Implements revocation mechanism (status list)
  • Provides holder self-service portal for revocation
  • Monitors for expired or expiring credentials

Security and compliance

  • All endpoints use TLS
  • Audit logging implemented
  • Security incident response plan in place
  • Key rotation procedure documented
  • Complies with FAPI 2.0 security profile (or equivalent)

Next steps

After implementing a HAIP-based high-assurance flow:

  1. Test in the sandbox: Use the German EUDI Wallet sandbox to validate your implementation
  2. Security review: Conduct security testing and penetration testing
  3. Performance testing: Ensure your source systems and issuance infrastructure can handle expected load
  4. User acceptance testing: Test the end-to-end user experience with real users
  5. Prepare for production: Complete trust framework registration, publish metadata, and prepare operational runbooks

Further reading