Skip to content

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

const state = generateRandomString(32);
const nonce = generateRandomString(32);

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:

{
  "alg": "ES256",
  "kid": "your-key-id",
  "typ": "JWT"
}

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:

{
  "alg": "ES256",
  "kid": "your-key-id",
  "typ": "JWT"
}

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:

  1. verification.evidence - Document details you want (e.g., document type, expiry date, issuer)
  2. 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:

GET https://your-app.com/callback
  ?code=SplxlOBeZQQYbYS6WxSbIA
  &state=your-state-value

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

{
  "id_token": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...",
  "expires_in": 3600
}

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