Skip to main content
Developer

Implementation Guide#

View as Markdown
On this page

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": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
  "code_challenge_method": "S256",
  "claims": { ... }
}

Understanding the OIDC4IA Claims Request#

The claims parameter follows the OpenID Connect for Identity Assurance (OIDC4IA) specification⁠ (opens in a new tab). It allows you to request specific verified identity claims and document evidence.

The claims request has two main parts:

  1. 1

    verification.evidence - Document details you want (e.g., document type, expiry date, issuer)

  2. 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 }
        }
      }
    }
  }
}

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": null,
                "personal_number": null
              }
            }
          ]
        },
        "claims": {
          "given_name": { "essential": true },
          "family_name": { "essential": true },
          "birthdate": { "essential": true },
          "nationalities": { "essential": true }
        }
      }
    }
  }
}

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": 60
}

Step 2: Redirect User to Authorization Endpoint#

Redirect the user to the authorization endpoint with the request_uri:

GET https://{dip-base-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
  &iss=https%3A%2F%2F{dip-base-url}

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...",
  "access_token": "not_applicable",
  "token_type": "Bearer",
  "expires_in": 900
}

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:

ClaimValidation
issMust match the DIP issuer URL
audMust match your client_id
expMust not be expired
iatMust not be in the future
nonceMust 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",
          "check_details": [
            {
              "check_method": "vcrypt",
              "result": "success"
            },
            {
              "check_method": "data",
              "organization": "id.politiet.no",
              "result": "valid"
            }
          ],
          "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": "12345678901"
          }
        }
      ]
    },
    "claims": {
      "given_name": "AASAMUND",
      "family_name": "OESTENBYEN",
      "picture": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
      "birthdate": "1990-01-15",
      "gender": "male",
      "nationalities": ["NOR"]
    }
  }
}

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#

Was this page helpful?