# Verifier Public Client Integration: Authorization Code Flow + PKCE



# Introduction

<article class="text-token-text-primary w-full focus:outline-none [--shadow-height:45px] has-data-writing-block:pointer-events-none has-data-writing-block:-mt-(--shadow-height) has-data-writing-block:pt-(--shadow-height) [&:has([data-writing-block])>*]:pointer-events-auto [content-visibility:auto] supports-[content-visibility:auto]:[contain-intrinsic-size:auto_100lvh] scroll-mt-[calc(var(--header-height)+min(200px,max(70px,20svh)))]" data-scroll-anchor="true" data-testid="conversation-turn-4" data-turn="assistant" data-turn-id="request-WEB:8988cf2c-0a30-4f9c-bd3a-e0b333f552ea-6" dir="auto" id="bkmrk-purpose-and-scope-th" tabindex="-1">#### Purpose and scope

This runbook explains how a public client, such as a web or mobile application, can integrate with the Verifier acting as an Authorization Server (AS) using the Authorization Code Flow with PKCE. It provides developers with the end-to-end steps required to obtain and use tokens securely, from initiating the authorization request to exchanging the authorization code and calling protected APIs.

Main aspects covered include:

<div class="text-base my-auto mx-auto pb-10 [--thread-content-margin:--spacing(4)] thread-sm:[--thread-content-margin:--spacing(6)] thread-lg:[--thread-content-margin:--spacing(16)] px-(--thread-content-margin)"><div class="[--thread-content-max-width:40rem] thread-lg:[--thread-content-max-width:48rem] mx-auto max-w-(--thread-content-max-width) flex-1 group/turn-messages focus-visible:outline-hidden relative flex w-full min-w-0 flex-col agent-turn" tabindex="-1"><div class="flex max-w-full flex-col grow"><div class="min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal [.text-message+&]:mt-1" data-message-author-role="assistant" data-message-id="bd3a2ebf-9cd4-4a12-aaa0-88bb3f96df18" data-message-model-slug="gpt-5" dir="auto"><div class="flex w-full flex-col gap-1 empty:hidden first:pt-[1px]"><div class="markdown prose dark:prose-invert w-full break-words light markdown-new-styling">- Integration of public clients with the Verifier using Authorization Code Flow + PKCE.
- Secure use of PKCE (Proof Key for Code Exchange) to prevent authorization code interception.
- OAuth 2.1 authorization\_code profile with code\_verifier and code\_challenge.
- Token acquisition and usage for accessing Verifier-protected resources.
- Security considerations, error handling, and observability.

</div></div></div></div></div></div>#### Intended audience

<div class="text-base my-auto mx-auto pb-10 [--thread-content-margin:--spacing(4)] thread-sm:[--thread-content-margin:--spacing(6)] thread-lg:[--thread-content-margin:--spacing(16)] px-(--thread-content-margin)"><div class="[--thread-content-max-width:40rem] thread-lg:[--thread-content-max-width:48rem] mx-auto max-w-(--thread-content-max-width) flex-1 group/turn-messages focus-visible:outline-hidden relative flex w-full min-w-0 flex-col agent-turn" tabindex="-1"><div class="flex max-w-full flex-col grow"><div class="min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal [.text-message+&]:mt-1" data-message-author-role="assistant" data-message-id="bd3a2ebf-9cd4-4a12-aaa0-88bb3f96df18" data-message-model-slug="gpt-5" dir="auto"><div class="flex w-full flex-col gap-1 empty:hidden first:pt-[1px]"><div class="markdown prose dark:prose-invert w-full break-words light markdown-new-styling">- Frontend developers integrating web or mobile applications with the Verifier.
- Technical integrators responsible for configuring the public client.
- SRE and security engineers reviewing client security compliance.

</div></div></div></div></div></div>#### High-level architecture

*embedded-image-AuthCodePKCE-arch.png*

<div class="text-base my-auto mx-auto pb-10 [--thread-content-margin:--spacing(4)] thread-sm:[--thread-content-margin:--spacing(6)] thread-lg:[--thread-content-margin:--spacing(16)] px-(--thread-content-margin)"><div class="[--thread-content-max-width:40rem] thread-lg:[--thread-content-max-width:48rem] mx-auto max-w-(--thread-content-max-width) flex-1 group/turn-messages focus-visible:outline-hidden relative flex w-full min-w-0 flex-col agent-turn" tabindex="-1"><div class="flex max-w-full flex-col grow"><div class="min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal [.text-message+&]:mt-1" data-message-author-role="assistant" data-message-id="bd3a2ebf-9cd4-4a12-aaa0-88bb3f96df18" data-message-model-slug="gpt-5" dir="auto"><div class="flex w-full flex-col gap-1 empty:hidden first:pt-[1px]"><div class="markdown prose dark:prose-invert w-full break-words light markdown-new-styling">1. The public client redirects the user to the Verifier Authorization Endpoint, including the code challenge (PKCE) and other OAuth parameters.
2. The user authenticates and grants consent.
3. The Verifier returns an authorization code to the client via redirection.
4. The client exchanges the authorization code for tokens at the Token Endpoint using the code\_verifier.
5. The Verifier issues access and ID tokens with limited lifetime.
6. The client uses the access token to call protected APIs.

</div></div></div></div></div></div>#### High-level flow

*embedded-image-AuthCodePKCE-flow.png*

<div class="text-base my-auto mx-auto pb-10 [--thread-content-margin:--spacing(4)] thread-sm:[--thread-content-margin:--spacing(6)] thread-lg:[--thread-content-margin:--spacing(16)] px-(--thread-content-margin)"><div class="[--thread-content-max-width:40rem] thread-lg:[--thread-content-max-width:48rem] mx-auto max-w-(--thread-content-max-width) flex-1 group/turn-messages focus-visible:outline-hidden relative flex w-full min-w-0 flex-col agent-turn" tabindex="-1"><div class="flex max-w-full flex-col grow"><div class="min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal [.text-message+&]:mt-1" data-message-author-role="assistant" data-message-id="bd3a2ebf-9cd4-4a12-aaa0-88bb3f96df18" data-message-model-slug="gpt-5" dir="auto"><div class="flex w-full flex-col gap-1 empty:hidden first:pt-[1px]"><div class="markdown prose dark:prose-invert w-full break-words light markdown-new-styling">1. The user initiates the login process from the public client, which sends an authorization request to the Verifier (AS).
2. After successful authentication and consent, the AS returns an authorization code.
3. The public client securely exchanges the authorization code and code\_verifier for tokens.
4. The AS issues an access token and an ID token.
5. The client uses the access token to access protected resources on the Verifier.
6. The resource server validates the access token and returns the requested data.

</div></div></div></div><div class="z-0 flex min-h-[46px] justify-start">  
</div><div class="mt-3 w-full empty:hidden"><div class="text-center">  
</div></div></div></div></article>

# Integration Steps

### Prerequisites

- The legal entity has completed onboarding in the DOME ecosystem.
- The LEAR has obtained a valid **LEAR Credential** through the Issuer service.
- The end user (employee, contractor, etc.) has received a **Verifiable Credential** issued by the LEAR of their organization.
- DID method supported: `did:key`.
- Access to developer documentation and environment URLs (Verifier Authorization Server and APIs).

---

### Step 1 – User credential issuance

The organization’s LEAR uses the Issuer service to issue a **Verifiable Credential** to the employee or user who will log in through the public client.

- The credential is bound to the user’s DID.
- It includes roles or permissions within the organization.
- The credential is stored in the user’s **Wallet application**, which can later present it during authentication.

**Outcome:**  
User holds a valid **LEAR Credential** stored in their wallet and ready to be presented during the login process.


### Step 2 – Client configuration

**Client type:** Public (web or mobile app).

- Obtain and store the assigned `client_id,` which can be a did:key or a unique identifier.
- Register the `redirect_uri`.
- Implement [PKCE support](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce) (`code_challenge` / `code_verifier`) to protect the authorization code exchange.
- Ensure secure handling of redirects and state parameters.

**Outcome:**  
Public client is configured and ready to initiate the OAuth 2.1 Authorization Code Flow with PKCE.

### Step 3 - Registering to the Verifier (Trusted Services List)

The relying party must be registered in the [Trusted Services List](https://github.com/DOME-Marketplace/trust-framework). The data must match with your client's configuration (see step 2).

<div align="left" dir="ltr" id="bkmrk-field-description-cl"><table><tbody><tr><td>**Field**

</td><td>**Description**

</td></tr><tr><td>clientId

</td><td>Should be a did:key or a unique identifier for your client.

</td></tr><tr><td>url

</td><td>The base URL of your service or application.

</td></tr><tr><td>redirectUris

</td><td>Must include all the URLs where you expect to receive authentication responses.

</td></tr><tr><td>scopes

</td><td>Currently, only openid\_learcredential is accepted. This scope allows your service to request the necessary credentials.

</td></tr><tr><td>clientAuthenticationMethods

</td><td>Must be set to \["none"\]

</td></tr><tr><td>authorizationGrantTypes

</td><td>Must be set to \["authorization\_code"\] and \["refresh\_token"\] if needed.

</td></tr><tr><td>postLogoutRedirectUris

</td><td>Include URLs where users should be redirected after they log out from your service.

</td></tr><tr><td>requireAuthorizationConsent

</td><td>Set to false.

</td></tr><tr><td>requireProofKey

</td><td>Set to true to force PKCE.

</td></tr><tr><td>jwkSetUrl

</td><td>Leave it blank.

</td></tr><tr><td>tokenEndpointAuthenticationSigningAlgorithm

</td><td>Must be set to ES256, as this is the only supported algorithm.

</td></tr></tbody></table>

</div>[![image.png](https://knowledgebase.dome-marketplace.eu/uploads/images/gallery/2026-03/scaled-1680-/image.png)](https://knowledgebase.dome-marketplace.eu/uploads/images/gallery/2026-03/image.png)


### Step 4 – Authorization request

The public client initiates the authentication process by redirecting the user to the Verifier’s **Authorization Endpoint**, including the required parameters:

- `client_id`: has to match the one in your client's configuration
- `redirect_uri`: has to match the one in your client's configuration
- `response_type=code`
- `scope =` `openid learcredential`
- `state` : random string
- `nonce`: random string (this will be added in the ID token, so it is recommended if you rely on the ID token)
- `code_challenge` ([derived](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce) from `code_verifier`)
- `code_challenge_method=S256`

**Non-normative example:**

<div align="left" dir="ltr" id="bkmrk-%7B-%C2%A0-%C2%A0-%22%40context%22%3A-%5B%22"><table><colgroup><col></col></colgroup><tbody><tr><td>GET /oidc/auth?  
client\_id=did:key:zDnaeUidLS8MbNQuHsnbd3xMvfk4baLZKeWiFV7UHAv9NsmUE  
&amp;redirect\_uri=https%3A%2F%2Fapp.client.org%2F  
&amp;response\_type=code  
&amp;scope=openid%20eidas  
&amp;nonce=1234567890abcdef1234567890abcdefXYZabc  
&amp;state=1234abcd5678efgh9012ijkl3456mnop7890qrst  
&amp;code\_challenge=AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdEfGhI  
&amp;code\_challenge\_method=S256  
Host: authserver.example.org</td></tr></tbody></table>

</div>**Outcome:**  
User is redirected to the Verifier login screen and authenticates using their Wallet and Verifiable Credential.

### Step 5 – Authorization response

After successful authentication and consent, the Authorization Server redirects the user back to the client with the authorization code and state.

**Non-normative example:**

<div align="left" dir="ltr" id="bkmrk-http%2F1.1-302-foundlo"><table><colgroup><col></col></colgroup><tbody><tr><td>HTTP/1.1 302 Found  
Location: https://app.client.org/?  
code=A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8S9t0U1v2W3x4Y5z6  
&amp;state=1234abcd5678efgh9012ijkl3456mnop7890qrst  
</td></tr></tbody></table>

</div>**Outcome:**  
The public client receives the authorization code and verifies that the returned `state` matches the one it initially sent.

### Step 6 – Token request

The client exchanges the received authorization code for tokens by calling the **Token Endpoint**.  
This request must include the original `code_verifier` used to generate the challenge.

**Non-normative example:**

<div align="left" dir="ltr" id="bkmrk-post-%2Foauth2%2Ftoken-h"><table><colgroup><col></col></colgroup><tbody><tr><td>POST /oauth2/token HTTP/1.1  
Host: authserver.example.org  
Content-Type: application/json  
Content-Length: 311

{  
 "grant\_type": "authorization\_code",  
 "client\_id": "https://app.client.com",  
 "code\_verifier": "b7f9a4b52e6347a1b8f2c3d1a6...9e0f1ABCxyz",  
 "code": "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8S9t0U1v2W3x4Y5z6",  
 "redirect\_uri": "https://app.client.org/"  
}

</td></tr></tbody></table>

</div>**Outcome:**  
The Verifier validates the code and code\_verifier and issues an access token and ID token.

### Step 7 – Token response

**Non-normative example:**

<div align="left" dir="ltr" id="bkmrk-http%2F1.1-200-okconte"><table><colgroup><col></col></colgroup><tbody><tr><td>HTTP/1.1 200 OK  
Content-Type: application/json  
Cache-Control: no-store

{  
 "access\_token": "eyJhbGciOiJFQ0RILUVTIiwiZ...qtAlx1oFIUpQQ",  
 "expires\_in": 3600,  
 "id\_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...p-QV30",  
 "scope": "openid profile email",  
 "token\_type": "Bearer"  
}

</td></tr></tbody></table>

</div>**Outcome:**

- `access_token` is used to call protected APIs on the Verifier.
- `id_token` identifies the authenticated user.
- Tokens have a limited lifetime (typically 1 hour).

### Step 7 – Use access token

The public client includes the access token in the `Authorization` header when calling Verifier-protected APIs:

<div align="left" dir="ltr" id="bkmrk-authorization%3A-beare"><table><colgroup><col></col></colgroup><tbody><tr><td>Authorization: Bearer eyJhbGciOiJFQ0RILUVTIiwiZ...

</td></tr></tbody></table>

</div>The Verifier validates the token, checks its signature and expiration, and grants access to the requested resources.

### Pitfalls to avoid

- Mismatch between `client_id` or `redirect_uri` in request and registration.
- Missing or incorrect `code_verifier` / `code_challenge`.
- Using a weak or predictable `state` value (CSRF risk).
- Reusing authorization codes.
- Not handling token expiration and refresh flow.
- Forgetting to set `Cache-Control: no-store` in responses.