Netherlands - Generic Functions for data exchange Implementation Guide
0.3.0 - ci-build
Netherlands
Netherlands - Generic Functions for data exchange Implementation Guide, published by Stichting Nuts. This guide is not an authorized publication; it is the continuous build for version 0.3.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/nuts-foundation/nl-generic-functions-ig/ and changes regularly. See the Directory of published versions
User authentication establishes the identity of an end-user (typically a healthcare professional) in cross-organizational data exchanges. While the Authentication section describes how organizations and service providers authenticate using Verifiable Credentials, this section extends that model to include authenticated end-users.
The result of user authentication is a User Consent Credential - a short-lived Verifiable Credential issued by a trusted Identity Provider (IDP) that attests that an authenticated user has authorized their organization to act on their behalf. This credential can be included in the Verifiable Presentation sent to an Authorization Server as part of the Request Access Token [GFI-004] transaction.
Healthcare professionals need to access patient data across organizational boundaries. The receiving organization's Authorization Server must verify:
The generic authentication model (see Authentication) addresses organization and service provider identity through long-lived Verifiable Credentials issued by authoritative registries. However, user identity presents additional challenges:
The user authentication solution must:
In addition to the terminology defined in Authentication, this section uses:
When a user at Organization A accesses resources at Organization B, the fundamental question is: "Is Organization A authorized to act on behalf of this user?"
The solution models this as a delegation of authority:
Statement: "User Alice consents to Organization A acting on her behalf"
┌───────────┐ ┌────────────────┐
│ Alice │ ── delegates to ──▶ │ Organization A │
│ (User) │ │ (Employer) │
└───────────┘ └────────────────┘
│ │
│ authenticates at │ presents delegation to
▼ ▼
┌───────────┐ ┌────────────────┐
│ IDP │ │ Organization B │
│ │ │ (Verifier) │
└───────────┘ └────────────────┘
The IDP acts as a trusted third party that:
This is fundamentally different from:
The consent represents broad delegation - the user trusts their employer to act appropriately on their behalf. This aligns with the employment relationship:
This is distinct from specific consent where each recipient (verifier) would need to be explicitly named. Specific consent would require more user interactions but provides stronger guarantees about where the user's identity is shared.
Audience binding (restricting who can use the attestation) happens at presentation time, not at consent time:
This approach keeps the consent portable while still preventing misuse through presentation-level binding.
The user authentication flow consists of two phases:
This attestation is encoded as a Verifiable Credential (the User Consent Credential) and included in the Verifiable Presentation alongside other credentials when requesting access tokens via GFI-004.
The following diagram shows the complete flow from initial login through credential issuance to accessing external resources.
Note: Steps 10-12 (the authorization redirect for credential issuance) are near-instant if the user has a valid browser session with the IDP and has previously consented. The user may only see a brief loading indicator.
Table: User Authentication - Actors and Transactions
| Actor | Transaction | Initiator or Responder | Optionality | Reference |
|---|---|---|---|---|
| User | Authenticate with IDP | Initiator | R | Session Establishment |
| Consent to credential issuance | Responder | R | Credential Issuance | |
| Identity Provider | Authenticate User | Responder | R | Session Establishment |
| Issue User Consent Credential | Initiator | R | Credential Issuance | |
| Resolve key material [GFI-001] | Responder | R | [GFI-001] | |
| Client (RP/EHR) | Initiate user authentication | Initiator | R | Session Establishment |
| Request User Consent Credential | Initiator | R | Credential Issuance | |
| Request Access Token [GFI-004] | Initiator | R | [GFI-004] | |
| Verifier (AS) | Verify User Consent Credential | Responder | R | Credential Verification |
| Resolve key material [GFI-001] | Initiator | R | [GFI-001] |
The user establishes an authentication session with the Identity Provider using standard OpenID Connect flows.
When the client needs to access external resources on behalf of the user, it requests a User Consent Credential from the IDP using the OpenID4VCI 1.0 Authorization Code Flow. This credential is short-lived to provide user liveness guarantees.
The Authorization Code Flow is used for credential issuance because it:
With an active session and pre-approved consent, steps 2-5 complete in milliseconds.
The client initiates credential issuance by redirecting the user to the IDP's authorization endpoint. The request uses RFC 9396 Rich Authorization Requests to specify the credential type:
GET /authorize?
response_type=code&
client_id=ehr.care-org.example.com&
redirect_uri=https://ehr.care-org.example.com/credential-callback&
authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22credential_configuration_id%22%3A%22UserIdentityCredential%22%7D%5D&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
state=xyz789
Host: idp.example.com
The authorization_details parameter (URL-decoded) contains:
[
{
"type": "openid_credential",
"credential_configuration_id": "UserIdentityCredential"
}
]
| Parameter | Required | Description |
|---|---|---|
response_type |
R | Must be code |
client_id |
R | The client's identifier at the IDP |
redirect_uri |
R | Where to send the authorization code |
authorization_details |
R | JSON array specifying the credential type per RFC 9396 |
code_challenge |
R | PKCE challenge (SHA256 of code_verifier, base64url-encoded) |
code_challenge_method |
R | Must be S256 |
state |
R | Opaque value for CSRF protection |
issuer_state |
O | Binds request to previous IDP context (e.g., from credential offer) |
prompt |
O | Controls IDP behavior (see below) |
prompt ParameterSince the credential represents user consent for delegation, the user should always be aware when a credential is issued. The prompt parameter controls the level of user interaction:
| Value | Behavior | Use Case |
|---|---|---|
consent |
Show consent screen (if session valid) | Default - user confirms delegation |
login |
Force re-authentication, then show consent | For sensitive operations requiring fresh authentication |
The prompt=none option (silent authentication) is not appropriate for consent credentials, as the user should be aware when authorizing their organization to act on their behalf.
When the user is redirected to the IDP:
The consent screen should clearly indicate:
If the user has previously consented to this credential type, the IDP may skip the consent screen (unless prompt=consent is specified).
The client exchanges the authorization code for an access token, including the PKCE verifier:
POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://ehr.care-org.example.com/credential-callback&
client_id=ehr.care-org.example.com&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Using the access token, the client requests the User Consent Credential from the credential endpoint. The request includes a proof parameter - a JWT signed by the organization's key. The IDP uses the kid from the proof JWT header to determine which DID should be used as the credential subject:
POST /credential HTTP/1.1
Host: idp.example.com
Authorization: Bearer {access_token}
Content-Type: application/json
{
"format": "jwt_vc_json",
"credential_definition": {
"type": ["VerifiableCredential", "UserConsentCredential"]
},
"proof": {
"proof_type": "jwt",
"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0Iiwia2lkIjoiZGlkOndlYjpjYXJlLW9yZy1hLmV4YW1wbGUuY29tI2tleTEifQ..."
}
}
The proof JWT structure:
// Header
{
"typ": "openid4vci-proof+jwt",
"alg": "ES256",
"kid": "did:web:care-org-a.example.com#key1"
}
// Payload
{
"iss": "ehr.care-org.example.com",
"aud": "https://idp.example.com",
"iat": 1704067200,
}
The IDP extracts the DID from the kid header (the part before #) and uses it as the credentialSubject.id in the issued credential. By verifying the proof signature, the IDP ensures the requesting organization controls the private key associated with that DID.
The IDP returns a User Consent Credential as a JWT-encoded Verifiable Credential:
{
"iss": "did:web:idp.example.com",
"sub": "did:web:care-org-a.example.com",
"iat": 1704067200,
"exp": 1704070800,
"nbf": 1704067200,
"jti": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5",
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://nuts.nl/credentials/v1"
],
"type": ["VerifiableCredential", "UserConsentCredential"],
"credentialSubject": {
"id": "did:web:care-org-a.example.com",
"actingFor": {
"id": "did:web:idp.example.com:users:alice",
"givenName": "Alice",
"familyName": "Smith",
"identifier": {
"system": "urn:oid:2.16.528.1.1007.3.1",
"value": "123456789"
}
},
"consentGiven": "2024-01-01T10:30:00Z"
}
}
}
The credential states: "The IDP attests that user Alice has consented to Organization A acting on her behalf."
| Property | Value | Rationale |
|---|---|---|
| Lifetime | Short (e.g., 5-60 minutes) | Provides user liveness guarantee |
| Subject | Organization's DID | The entity receiving the delegation |
| Issuer | IDP's DID | Trusted third party that authenticated the user |
The User Consent Credential is included in the Verifiable Presentation sent to the Authorization Server as part of GFI-004.
The VP contains multiple credentials:
The VP's aud claim specifies the intended verifier, providing audience binding for the entire presentation:
{
"iss": "did:web:care-org-a.example.com",
"aud": "did:web:care-org-b.example.com",
"iat": 1704067200,
"exp": 1704067260,
"jti": "urn:uuid:6c46d6e2-5b3a-4e7d-9f8a-1b2c3d4e5f6a",
"vp": {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiablePresentation"],
"verifiableCredential": [
"<organization-credential-jwt>",
"<user-consent-credential-jwt>"
]
}
}
Note that the VP is signed by care-org-a (the holder) and the aud is set to care-org-b (the verifier). The verifier checks that the User Consent Credential's subject matches the VP issuer.
The Authorization Server verifies the User Consent Credential as part of processing the access token request.
aud claim matches the Authorization Server's identifier.iss claim identifies the presenter.credentialSubject.id) matches the VP's issuer (iss). This ensures the organization presenting the VP is the same organization that received the user's consent.iat, nbf, and exp claims on the credential.actingFor claims for authorization decisions and audit logging.The holder binding check (step 5) is critical. It ensures that only the organization named in the credential can present it:
VP:
iss: did:web:care-org-a.example.com ← presenter
aud: did:web:care-org-b.example.com ← verifier
User Consent Credential (inside VP):
credentialSubject.id: did:web:care-org-a.example.com ← must match VP.iss
If VP.iss ≠ credentialSubject.id, the credential is being presented by an organization other than the one that received the consent, and MUST be rejected.
The Authorization Server must maintain a list of trusted Identity Providers. This trust can be established through:
The User Consent Credential contains claims about the delegation of authority from user to organization.
| Claim | Required | Description |
|---|---|---|
id |
R | The organization's DID (the entity receiving the delegation) |
actingFor |
R | Object containing the user's identity claims |
consentGiven |
O | Timestamp when consent was given |
actingFor)| Claim | Required | Description |
|---|---|---|
id |
R | The user's DID at the IDP |
givenName |
O | User's given name |
familyName |
O | User's family name |
identifier |
O | National identifier (e.g., BSN pseudonym, UZI number) |
assuranceLevel |
O | Level of identity assurance (e.g., eIDAS level) |
The specific claims required depend on the use case and should be defined by the applicable trust framework.
User Consent Credentials should be short-lived (recommended: 5-60 minutes) to:
Audience binding occurs at the Verifiable Presentation level, not the credential level (see Credential Semantics). The VP's aud claim specifies the intended recipient:
aud claim matches its own identifier| Specification | Relationship |
|---|---|
| Authentication | User authentication extends the base authentication model with user consent |
| GFI-001 | IDP's DID is resolved to verify credential signatures |
| GFI-002 | User Consent Credential issuance uses OpenID4VCI |
| GFI-004 | User Consent Credential is included in the VP for access token requests |
| Identification | User identifiers follow the identification guidelines |
This example shows the complete flow for a healthcare professional accessing patient data at an external care provider.
The user logs into the EHR system. The client redirects to the IDP for authentication:
GET /authorize?response_type=code&client_id=ehr.care-org.example.com&redirect_uri=https://ehr.care-org.example.com/callback&scope=openid%20profile&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=abc123 HTTP/1.1
Host: idp.example.com
User authenticates via DigiD. IDP redirects back:
HTTP/1.1 302 Found
Location: https://ehr.care-org.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=abc123
POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fehr.care-org.example.com%2Fcallback&client_id=ehr.care-org.example.com&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Response:
{
"access_token": "session_token_xyz",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
User now works in the EHR system…
When the user needs to access data at Care Provider X, the client initiates OpenID4VCI authorization code flow:
GET /authorize?response_type=code&client_id=ehr.care-org.example.com&redirect_uri=https://ehr.care-org.example.com/credential-callback&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22credential_configuration_id%22%3A%22UserIdentityCredential%22%7D%5D&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=xyz789 HTTP/1.1
Host: idp.example.com
Since the user has a valid session, the IDP immediately redirects back (or shows a brief consent screen for first-time access):
HTTP/1.1 302 Found
Location: https://ehr.care-org.example.com/credential-callback?code=Qcb0Orv1zh&state=xyz789
POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=Qcb0Orv1zh&redirect_uri=https%3A%2F%2Fehr.care-org.example.com%2Fcredential-callback&client_id=ehr.care-org.example.com&code_verifier=another_verifier_string
The client includes a proof JWT signed with the organization's key:
POST /credential HTTP/1.1
Host: idp.example.com
Authorization: Bearer {credential_access_token}
Content-Type: application/json
{
"format": "jwt_vc_json",
"credential_definition": {
"type": ["VerifiableCredential", "UserConsentCredential"]
},
"proof": {
"proof_type": "jwt",
"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0Iiwia2lkIjoiZGlkOndlYjpjYXJlLW9yZy1hLmV4YW1wbGUuY29tI2tleTEifQ..."
}
}
{
"credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImlkcC1rZXktMSJ9...",
"format": "jwt_vc_json"
}
The client creates a Verifiable Presentation containing the Organization Credential and the User Consent Credential:
POST /oauth/token HTTP/1.1
Host: auth.care-provider-x.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...
The assertion JWT contains a VP (with aud set to Care Provider X) containing:
{
"access_token": "resource_access_token_abc",
"token_type": "DPoP",
"expires_in": 300
}
The client can now access protected resources at Care Provider X using this access token.
actingFor) or in a separate Employment Credential issued by the organization?