Implementation Guide¶
This guide walks you through implementing the complete OIDC4IA authentication flow with DIP.
Step 1: Create the PAR Request¶
1.1 Generate PKCE Values¶
Generate a code_verifier (43-128 character random string) and compute the code_challenge:
// Generate code_verifier (43-128 characters)
const codeVerifier = generateRandomString(43);
// Compute code_challenge = BASE64URL(SHA256(code_verifier))
const codeChallenge = base64url(sha256(codeVerifier));
1.2 Generate State and Nonce¶
Store these values in your session for later verification.
1.3 Build the Client Assertion JWT¶
The client assertion authenticates your client. See Authentication for full details.
Header:
Payload:
{
"iss": "your_client_id",
"sub": "your_client_id",
"aud": "https://{dip-base-url}",
"exp": 1759835872
}
1.4 Build the Request Object JWT¶
The request object contains the authorization parameters, including the OIDC4IA claims parameter that specifies which verified identity data you need.
Header:
Payload:
{
"iss": "your_client_id",
"sub": "your_client_id",
"aud": "https://{dip-base-url}",
"exp": 1759835872,
"client_id": "your_client_id",
"response_type": "code",
"redirect_uri": "https://your-app.com/callback",
"scope": "openid",
"state": "your-state-value",
"nonce": "your-nonce-value",
"code_challenge": "K2-ltc83acc4h0c9w6ESC_rEMTJ3bww-uCHaoeK1t8U",
"code_challenge_method": "S256",
"claims": { ... }
}
Understanding the OIDC4IA Claims Request¶
The claims parameter follows the OpenID Connect for Identity Assurance (OIDC4IA) specification. It allows you to request specific verified identity claims and document evidence.
Request only what you need
Only include the claims and document details that your application requires. This follows the principle of data minimization - you should not request more personal data than necessary for your use case.
The claims request has two main parts:
verification.evidence- Document details you want (e.g., document type, expiry date, issuer)claims- Identity claims you want (e.g., name, birthdate, nationality)
Minimal Example¶
If you only need the user's given name, family name, and birthdate (no document details):
{
"claims": {
"id_token": {
"verified_claims": {
"verification": {
"trust_framework": { "value": "stoe" }
},
"claims": {
"given_name": { "essential": true },
"family_name": { "essential": true },
"birthdate": { "essential": true }
}
}
}
}
}
Note
The evidence array is optional. Omit it entirely if you only need identity claims and don't need document details (type, expiry date, issuer, etc.).
Full Example¶
If you need identity claims plus document verification details:
{
"claims": {
"id_token": {
"verified_claims": {
"verification": {
"trust_framework": { "value": "stoe" },
"evidence": [
{
"type": { "value": "document" },
"document_details": {
"type": null,
"document_number": null,
"date_of_expiry": null,
"issuer": {
"country_code": null,
"name": null
},
"personal_number": null,
"issuer_check": null
}
}
]
},
"claims": {
"given_name": { "essential": true },
"family_name": { "essential": true },
"birthdate": { "essential": true },
"nationalities": { "essential": true }
}
}
}
}
}
Note
Fields set to null in document_details indicate you want that data returned. Omit fields entirely if you don't need them.
For the complete list of available claims and document details, see the PAR Request documentation.
1.5 Send the PAR Request¶
POST /par HTTP/1.1
Host: {dip-base-url}
Content-Type: application/x-www-form-urlencoded
client_id=your_client_id
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6...
&request=eyJhbGciOiJFUzI1NiIsImtpZCI6...
1.6 PAR Response¶
{
"request_uri": "urn:ietf:params:oauth:request_uri:550e8400-e29b-41d4-a716-446655440000",
"expires_in": 600
}
Step 2: Redirect User to Authorization Endpoint¶
Redirect the user to the authorization endpoint with the request_uri:
GET https://{dip-flow-url}/auth
?client_id=your_client_id
&request_uri=urn:ietf:params:oauth:request_uri:550e8400-e29b-41d4-a716-446655440000
The user is then handed off to the BankID app where they complete identity verification (document scanning and facial recognition).
Step 3: Handle the Callback¶
After verification, the BankID app redirects the user back to your redirect_uri with an authorization code:
Warning
Always verify that the state matches what you stored in Step 1.2 to prevent CSRF attacks.
Step 4: Exchange Code for Tokens¶
4.1 Build a New Client Assertion¶
Create a fresh client assertion JWT (same format as Step 1.3).
4.2 Send the Token Request¶
POST /token HTTP/1.1
Host: {dip-base-url}
Content-Type: application/x-www-form-urlencoded
client_id=your_client_id
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6...
&grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&redirect_uri=https://your-app.com/callback
4.3 Token Response¶
Step 5: Decrypt and Validate the ID Token¶
The id_token is a JWE (JSON Web Encryption) containing a signed JWT.
5.1 Decrypt the JWE¶
Use your private key to decrypt the JWE and extract the signed JWT.
5.2 Verify the JWT Signature¶
Fetch the DIP public keys from the JWKS endpoint and verify the signature.
5.3 Validate Claims¶
Verify the following claims:
| Claim | Validation |
|---|---|
iss |
Must match the DIP issuer URL |
aud |
Must match your client_id |
exp |
Must not be expired |
iat |
Must not be in the future |
nonce |
Must match the nonce from Step 1.2 |
5.4 Extract Verified Claims¶
The user's verified identity data is in the verified_claims object. See ID Token for the complete structure.
Example ID Token payload:
{
"iss": "https://{dip-base-url}",
"sub": "hashed-subject-id",
"aud": "your_client_id",
"exp": 1759835872,
"iat": 1759832272,
"auth_time": 1759832272,
"nonce": "your-nonce-value",
"acr": "urn:bankid:idcheck",
"amr": ["face", "user"],
"verified_claims": {
"verification": {
"trust_framework": "stoe",
"evidence": [
{
"type": "document",
"document_details": {
"type": "passport",
"document_number": "AB1234567",
"date_of_issuance": "2020-01-15",
"date_of_expiry": "2030-01-15",
"issuer": {
"country_code": "NOR",
"name": "Politiet"
},
"personal_number": {
"type": "no-fnr",
"value": "12345678901"
},
"active_authentication_result": "passed",
"issuer_check": {
"valid": "VALID"
}
}
}
]
},
"claims": {
"given_name": "AASAMUND",
"family_name": "OESTENBYEN",
"picture": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"birthdate": "1990-01-15",
"gender": "male",
"nationalities": ["NOR"]
}
}
}
Note
Any requested evidence or claims that are not available in the ID check data will be omitted from the response. The response only contains data that was successfully retrieved.
Complete Flow Summary¶
sequenceDiagram
participant Client
participant DIP
participant BankID as BankID App
participant User
Client->>DIP: 1. POST /par
DIP-->>Client: 2. request_uri
Client->>BankID: 3. Redirect to /auth
User->>BankID: 4. Complete verification
BankID->>DIP: 5. Request auth code
DIP-->>BankID: 6. Return auth code
BankID->>Client: 7. Redirect with code
Client->>DIP: 8. POST /token
DIP-->>Client: 9. Encrypted ID token
Client->>Client: 10. Decrypt and validate
Next Steps¶
- Authentication - Detailed JWT requirements
- PAR Request - Full PAR documentation
- Token Request - Full token endpoint documentation
- ID Token - Complete ID token structure