← Back to list

Sender-Constrained DPoP + JAR/PAR OIDC Flow (Browser to RP): Full Technical Design

1. Executive Summary

Declerus Yves Kerbens · 2025-07-22 23:51 · 16 claps · 13.9 min read
#openid-connect #dpop #session-management #fapi #oauth2
Open on Medium ↗
Wiki topics: FT · Fine-tuning & Adaptation BIZ · Business Strategy 🥊 · Combat Sports

Sender-Constrained Session Cookie: DPoP + JAR OIDC Flow (Browser to RP)

1. Executive Summary

This document specifies a secure browser-to-RP authentication architecture that enforces true sender-constrained Proof-of-Possession (DPoP, RFC 9449) for OIDC Authorization Code flows, utilizing JWT-Secured Authorization Requests (JAR, RFC 9101).

The central contribution is a mechanism that transforms a standard bearer session cookie into a high-assurance, mTLS-like credential, cryptographically binding it to a browser-held key. This protects against session hijacking, even in the event of a compromised server or by a malicious administrator.

This is achieved by leveraging the OIDC flow as a trusted, third-party ceremony. The Relying Party (RP) orchestrates the process by embedding the browser’s DPoP key thumbprint (jkt) within a signed JAR sent to the Authorization Server (AS/OIDC Provider). The AS authenticates the user, allowing the RP to securely bind the resulting session to the browser’s key. To ensure broad interoperability, the architecture prioritizes using the standard state parameter for this binding for its universal interoperability, while remaining compatible with Authorization Servers that support the first-class dpop_jkt request parameter, although this later should be used if supported by AS. For clarity, subsequent discussion in this document will focus on the state parameter, but the principles apply equally where dpop_jkt is supported.

The result is a high-assurance session where user impersonation via stolen credentials is cryptographically prevented, closing a critical gap in standard confidential client architectures.

2. The Trust Boundary Problem: Client Types and Session Security

To understand the security gap this architecture closes, it is essential to distinguish between the two primary client types defined in OAuth2/OIDC and their respective trust boundaries. The vulnerability lies in how the session between the user’s browser and the application backend (the Relying Party) is managed.

Public Clients, such as browser-based Single-Page Applications (SPAs), are incapable of maintaining secrets. The recommended pattern involves exchanging an authorization code for tokens directly in the browser. While this can be secured using DPoP, it places the full burden of complex and secure token management on the client-side application.

Confidential Clients, such as a Backend-for-Frontend (BFF), are the industry-recommended best practice. Here, the browser is redirected to the /authorize endpoint, but only the secure backend ever communicates with the /token endpoint. This correctly keeps all tokens off the browser. However, to maintain the user’s session, the BFF must issue a session cookie.

This is where the critical security gap appears. This session cookie, by its very nature, is a bearer token. Standard protections like HttpOnly and Secure flags are vital, but they only protect the cookie from being stolen by other websites or over insecure connections. They do nothing to prove that the entity presenting the cookie is the same browser that originally authenticated.

Anyone who can steal this cookie — for instance, by exploiting a file inclusion vulnerability to read the server’s session files, or an administrator with direct access to the Redis session store — can impersonate the user with impunity. The server has no cryptographic way to challenge the requestor and verify it is the legitimate user’s browser.

This is the precise gap this proposal addresses. By introducing a mechanism to cryptographically bind the session cookie to a private key held securely within the browser, we transform the cookie from a simple bearer token into a verifiable, sender-constrained credential. This closes the last and weakest link in the high-security session authentication chain for confidential clients.

3. Foundational OIDC Mechanisms

The proposed architecture combines several advanced OIDC and OAuth 2.0 specifications.

  • JWT-Secured Authorization Request (JAR, RFC 9101): Standard OIDC requests pass parameters in a URL query string, which is vulnerable to tampering. JAR solves this by packaging all authorization parameters into a JWT signed by the client (the RP). In this proposal, the JAR includes a custom claim representing the browser’s DPoP key thumbprint (dpop_jkt). This ensures the integrity and authenticity of the request and securely binds the authorization process to the browser’s DPoP key.
  • **state Parameter (RFC 6749):** The state parameter is an opaque value created by the client (the RP) to maintain state between the authorization request and the callback. Its primary security function is to mitigate Cross-Site Request Forgery (CSRF). Its arbitrary nature makes it the perfect vehicle for correlating the browser's key registration, the challenge-response proof, and the final OIDC callback.
  • Demonstrating Proof-of-Possession (DPoP, RFC 9449): DPoP is the mechanism for sender-constraining bearer tokens. A client proves it possesses a private key by signing a proof-JWT on each request. The key thumbprint, or **jkt (as defined in RFC 7638**), is used by the Authorization Server to bind the issued Access Token to the hash of the client’s public key.
  • dpop_jkt Authorization-Request Parameter (RFC 7638, optional): Some ASes support a first-class **dpop_jkt query parameter. When present, the AS computes the `jWk`** thumbprint of the public key in the incoming DPoP proof and verifies it matches this parameter, binding the issued token to that key without needing to leak it via state.

The synergy of these standards is what enables this proposal: by having the RP include the browser’s DPoP key thumbprint (dpop_jkt) as a claim inside the signed JAR, and by using the state parameter to correlate all steps, the Authorization Server can cryptographically bind the resulting tokens to the browser’s key, with integrity protected by the RP’s JAR signature.

4. State-of-the-Art Analysis & The Identified Gap

The Current Standard: Bearer Token Session Cookies

In a standard BFF architecture, user sessions are managed with a bearer session cookie, secured by protections recommended by the OWASP Session Management Cheat Sheet:

  • **HttpOnly Flag:** Prevents client-side scripts from accessing the cookie.
  • **Secure Flag:** Ensures the cookie is only sent over HTTPS.
  • **SameSite Flag (Lax or Strict):** Protects against CSRF attacks.

While effective against external threats, this model assumes the Relying Party (the BFF server) is secure and can be trusted.

Current DPoP Implementations and Their Limitations

Current DPoP implementations focus on two primary scenarios, neither of which addresses securing a session cookie against an insider threat:

  1. The SPA Pattern: The browser holds the DPoP key and the Access Token. This correctly constrains the token but requires complex and risky client-side token management.
  2. The BFF Pattern (Server-Side DPoP): The BFF holds the DPoP key and uses it to protect calls to a downstream Resource Server. This does nothing to secure the session between the browser and the BFF, which still uses a standard bearer session cookie.

The Identified Gap: The Compromised Server Threat Model

The standard model fails when the threat model includes a compromised server or a malicious internal administrator. An attacker with server access can:

  • Steal Session IDs Directly: Access the session store (e.g., Redis, database) and exfiltrate active session IDs.
  • Impersonate Any User: Use a stolen session ID to make requests, and the BFF will trust them.
  • Bypass Session Logic: Modify or bypass session validation logic entirely.

The HttpOnly cookie provides no protection in this scenario. The trust anchor of the system—the server—is broken.

The Proposal: On-Demand Client Certification

This architecture directly addresses the identified gap by transforming the session cookie from a simple bearer token into a cryptographically-bound identifier. It establishes a secure channel analogous to mTLS, but without requiring pre-provisioned client certificates in the browser.

  • The OIDC Flow as a Trusted Identity Link. The standard OIDC flow is used as a trusted mechanism to link an authenticated user’s identity to a specific browser’s public key. This is achieved by the Relying Party (RP), which embeds the browser’s DPoP key thumbprint (dpop_jkt) into the state parameter of the OIDC authorization request. The Authorization Server (AS) treats the state parameter as opaque, simply returning it to the RP after the user authenticates. This allows the RP to securely associate the validated user identity with the browser's key.
  • The Challenge as Proof-of-Possession. The RP can optionally generate an encrypted challenge-response flow. Before the main OIDC redirection, the browser must solve this challenge using its private key, to partially proving it possesses the key corresponding the public key thumbprint it sent to the RP.
  • The RP-Enforced Binding. After the user authenticates at the AS, the RP receives the authorization code and the structured state parameter containing the dpop_jkt. The RP validates the pre-authentication proof (if used) and exchanges the code to confirm the user's identity. At this point, the RP has cryptographically verified proof of the user's identity, the browser's public key, and possession of the private key. It then binds the dpop_jkt to the new session in its own server-side session store.
  • Session Duration vs. Proof-of-Possession. The user’s “logged-in” window remains defined solely by the cookie’s TTL (for example, 30 minutes or 8 hours). What changes is that every HTTP request during that window must also include a freshly signed DPoP proof JWT (with its own jti, iat, htu/htm). The cookie controls how long the session lasts; the DPoP proof controls who is using it.
  • The Bound Session. The resulting session cookie, while still an identifier, is now useless on its own because it’s cryptographically bound to the browser’s key. An attacker who steals the session ID from a compromised server cannot generate the required DPoP proofs for subsequent requests, rendering the stolen credential inert.

This model elevates session security to protect against insider threats and server compromise, a critical requirement for any system where user impersonation must be prevented at all costs.

The Distributed Trust Model

The security of this architecture is rooted in a clear distribution of trust and responsibility between the Relying Party (RP) and the Authorization Server (AS), with the state parameter acting as the cryptographic link.

  • The RP Establishes Browser-to-Session Binding: The Relying Party is solely responsible for verifying the link between the authenticated user and the browser. It achieves this by using the state parameter to correlate two key events:
  1. Proof of Possession: The browser proves it owns the private key by solving a challenge sent by the RP.
  2. Proof of Authentication: The AS confirms the user’s identity and returns the same state value to the RP.
  • By tying these events together, the RP can confidently bind the user’s new session to the browser’s DPoP key, securing the Browser-to-RP channel.
  • The AS Acts as the Identity Trust Anchor: In this model, the Authorization Server’s role is not altered. It is exclusively responsible, as per the OIDC standard, for its critical task: authenticating the user. After a successful login, it returns a standard authorization code and the original state parameter. This standard response is all the RP needs to proceed, as it provides a verifiable signal that the user is who they claim to be.

This model creates a high-assurance session where the Relying Party has a cryptographic guarantee that the session cookie is being used by the same browser that authenticated, leveraging the Authorization Server as the trusted source for user identity.

5. Sequence Overview

Actors

  • Browser: The end-user’s client, which generates a DPoP keypair, solves a cryptographic challenge, and authenticates at the AS.
  • RP (Next.js): The Relying Party (your Next.js backend), which orchestrates the flow, holds the access token, and communicates with the Resource Server.
  • AS/OP: The Authorization Server, which supports JAR and the standard OIDC flow.
  • RS: The Resource Server, which hosts the protected API and validates the DPoP-bound access tokens.

Full Sequence

[BROWSER]               [RP (Next.js)]                [AS]                  [RS]
   |                          |                        |                        |
1. |-- Generate DPoP key -----|                        |                        |
2. |-- pubJWK/jkt ----------->|                        |                        |
3. |                          | [Build JAR w/ jkt]     |                        |
4. |                          | [Generate challenge]    |                        |
5. |<-------------------------| enc_challenge, JAR, state                      |
6. |-- [Validate JAR:         |                        |                        |
   |     - Verify signature   |                        |                        |
   |     - Extract jkt        |                        |                        |
   |     - Compare local jkt] |                        |                        |
7. |-- Solve challenge -------|                        |                        |
   |   (decrypt, sign,        |                        |                        |
   |    prepare proof)        |                        |                        |
8. |-- Send challenge ------- >|                        |                        |
   |   response + state       |                        |                        |
   |   (to dedicated RP API)  |                        |                        |
9. |-- Redirect to ---------->|                        |                        |
   |   /authorize endpoint    |                        |                        |
   |   (JAR as 'request',     |                        |                        |
   |    state as 'state')     |                        |                        |
10.|                          |                        |                        |
   |                          |    [User authenticates at AS]                   |
   |<-------------------------|<-----------------------| browser redirected with |
   |                          |                        | code, state            |
11.|                          | [Checks:]              |                        |
   |                          |  - OIDC code matches state                      |
   |                          |  - DPoP proof matches state                     |
   |                          |                        |                        |
12.|                          | [Exchanges code for    |                        |
   |                          |  DPoP-bound AT] <------|                        |
13.|<-------------------------| DPoP-Bound Session     |                        |
   |                          | cookie issued (RP holds|                        |
   |                          | AT)                    |                        |
   |                          |                        |                        |
   - - - - - - - - - - - - -  AUTHENTICATION COMPLETE  - - - - - - - - - - - - -

6. Step-by-Step Protocol

A. DPoP Key Registration, Challenge, and OIDC Initiation

  1. Browser: Generates a new DPoP keypair (e.g., ECDSA P-256 via Web Crypto API), ensuring the private key is non-extractable.
  2. Browser: Computes the JWK Thumbprint (dpop_jkt) of the public key.
  3. Browser: POSTs its public JWK to a dedicated RP endpoint (e.g., /api/auth/login).
  4. RP: Receives the public JWK and dpop_jkt.
  5. RP: Generates a cryptographically secure random challenge (nonce).
  6. RP: Encrypts the nonce using the browser’s public key to create an enc_challenge.
  7. RP: Generates a separate, cryptographically secure random value (csrf_token) for the state parameter's CSRF protection.
  8. RP: Creates a structured state parameter by combining the CSRF token and the browser's dpop_jkt.
  • Example: state = "{csrf_token}:{dpop_jkt}"
  • The state value does sill contain a high-entropy random token, which makes it unpredictable and effective at preventing Cross-Site Request Forgery attacks.

9. RP: Builds a standard JWT-Secured Authorization Request (JAR).

10. RP: Constructs the final OIDC /authorize endpoint URL.

11. RP: Responds to the browser with { enc_challenge, authorize_url }. The state value within the authorize_url now contains the key thumbprint.

B. Browser Solves Challenge

  1. Browser: Validates the signed JAR by verifying the JAR signature using the RP’s public key.
  2. Browser: Extracts the dpop_jkt from the JAR and compares it to its own locally stored value. If any validation fails, the flow is aborted.
  3. Browser: Decrypts the enc_challenge (nonce) using its private DPoP key.
  4. Browser: Signs the decrypted challenge to create a proof.
  5. Browser: POSTs { proof, state } to a dedicated RP endpoint (e.g., /api/auth/challenge-response).
  • The RP stores this proof, associating it with the state.
  • No session is issued yet.

C. OIDC Authentication and Session Issuance

  1. Browser: Redirects to the Authorization Server’s /authorize endpoint by navigating to the provided authorize_url.
  2. AS: Presents the authentication UI; the user logs in at the AS.
  3. AS: Redirects the browser back to the RP’s OIDC callback endpoint with an authorization code and the original state.
  4. RP: Upon receiving the callback, the RP performs the final validation for the given state:
  • It validates the stored DPoP proof using the public key and challenge (nonce) associated with the state.
  • It exchanges the OIDC code for a **DPoP Bound Access Token with the AS and issue `DPoP-bound Session Cookie`**.

5. RP: Only if both the DPoP proof and the OIDC code are valid, the RP stores the received Access Token and establishes a secure session with the browser (e.g., via an HttpOnly cookie).

7. Security Properties

Customer Impersonation Prevention:

  • A compromised server or malicious admin cannot fabricate a user session or transaction because they do not possess the user’s private DPoP key required to generate the initial proof.

Session Replay Prevention:

  • The cryptographic challenge-response mechanism (using a signed challenge and proof) prevents the replay of authentication attempts.

Key Binding:

  • The dpop_jkt in the JAR ensures that the Authorization Server binds the authorization code and resulting access token to the browser’s specific DPoP key, established at the very start of the flow.

State Correlation:

  • The state parameter links all stages of the flow, preventing CSRF and session fixation attacks by ensuring that only the initiating browser can complete the flow.

End-to-End Visibility:

  • At no point does the RP or the AS issue a session or token to a browser whose key has not been proven and matched; every transition is cryptographically checked by at least two parties.

8. Implementation Example (Node.js / Next.js Pseudocode)

A. Browser (Frontend)

// 1. Generate DPoP keypair (non-extractable)
const keyPair = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  false, // Non-extractable
  ["sign", "verify"]
);

// 2. Compute JWK Thumbprint
const pubJWK = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
const dpop_jkt = await calculateJwkThumbprint(pubJWK); // Implement per RFC 7638
// 3. Register public JWK with RP
const regRes = await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ pubJWK }),
});
const { enc_challenge, JAR, state, authorize_url } = await regRes.json();

B. RP (Next.js API) — High Level

// RP (Next.js API) — High Level Authentication Flow
// Use a robust, distributed cache like Redis for production
const flowCache = new Map(); 
// --- 1. Initiate Login and DPoP Key Registration ---
// Endpoint: /api/auth/login
async function dpopRegister(req, res) {
  const { pubJWK } = req.body;
  const dpop_jkt = await calculateJwkThumbprint(pubJWK);
  const challenge = crypto.randomBytes(32);
  const state = crypto.randomBytes(16).toString('hex');
  // Encrypt the challenge nonce with the browser's public key
  const enc_challenge = encryptWithPublicKey(pubJWK, challenge); // Implement ECIES/ECDH+AES-GCM
  // Build the JWT-Secured Authorization Request (JAR)
  const jarPayload = {
    client_id: "YOUR_CLIENT_ID",
    response_type: "code",
    scope: "openid profile",
    redirect_uri: "<https://rp.example.com/api/auth/callback>",
    // This claim informs the AS about the browser's key, enabling DPoP binding
    browser_cnf: { jkt: dpop_jkt }, 
    // ...other OIDC params
  };
  const JAR = await signJwt(jarPayload, rpPrivateKey); // Your custom JWT signing function
  // Construct the full /authorize endpoint URL for the browser
  const authorize_url = `https://as.example.com/authorize?request=${encodeURIComponent(JAR)}&state=${encodeURIComponent(state)}`;
  // Temporarily store flow data, correlated by the state parameter
  flowCache.set(state, { pubJWK, challenge, dpop_jkt, JAR, status: 'initiated' });
  res.json({ enc_challenge, JAR, state, authorize_url });
}
// --- 2. Receive and Store the Browser's Proof-of-Possession ---
// Endpoint: /api/auth/challenge-response
async function challengeResponse(req, res) {
  const { proof, state } = req.body;
  const flowData = flowCache.get(state);
  // Ensure this is the correct step in the flow for the given state
  if (flowData && flowData.status === 'initiated') {
    flowData.proof = proof; // Store the browser's signed proof
    flowData.status = 'proof_received';
    flowCache.set(state, flowData);
    res.status(200).json({ message: "Proof received. Proceed to OIDC authentication." });
  } else {
    res.status(400).json({ error: "Invalid state or flow order." });
  }
}
// --- 3. Handle the OIDC Callback and Issue the Session ---
// Endpoint: /api/auth/callback (OIDC Redirect URI)
async function oidcCallback(req, res) {
  const { code, state } = req.query;
  const flowData = flowCache.get(state);
  // Security check: Ensure we have received the DPoP proof for this flow
  if (!flowData || flowData.status !== 'proof_received') {
    return res.status(403).send("Access Denied: DPoP proof missing or invalid state.");
  }
  // 1. Verify the browser's DPoP proof from the initial challenge
  const isProofValid = await verifyECDSASignature(
    flowData.proof,
    flowData.challenge, // The original nonce
    flowData.pubJWK     // The browser's public key
  );
  if (!isProofValid) {
    flowCache.delete(state); // Clean up failed attempt
    return res.status(403).send("Access Denied: Invalid DPoP signature.");
  }
  // 2. Exchange the OIDC authorization code for tokens
  // This request to the AS MUST also include the browser's DPoP proof
  const tokenSet = await exchangeCodeForTokens(code, flowData.proof);
  // 3. Issue the secure, HttpOnly session cookie
  // The session store MUST contain the DPoP-bound access token and the browser's public key
  issueSessionAndStoreToken(req, res, {
    userInfo: tokenSet.claims(),
    accessToken: tokenSet.access_token,
    pubJWK: flowData.pubJWK // <-- CRITICAL: Store the key with the session
  });
  // Clean up the temporary flow data from the cache
  flowCache.delete(state);
  // Redirect the user to the main application page
  res.redirect('/dashboard'); 
}

B. Browser Verification (Next.js API) — High Level

// 1. Validate JAR (browser-side!)
if (!await verifyJarSignature(JAR, rpPublicKey)) throw new Error("Invalid JAR signature");
const jarPayload = decodeJwtPayload(JAR); // custom function to decode JAR
if (jarPayload.browser_cnf?.jkt !== dpop_jkt) throw new Error("Key mismatch");

// 2. Decrypt challenge
const decryptedChallenge = await decryptWithPrivateKey(keyPair.privateKey, enc_challenge); // implement ECDH/ECIES
// 3. Sign decrypted challenge (nonce)
const proof = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" },
  keyPair.privateKey,
  decryptedChallenge
);
// 4. Send proof to RP
await fetch('/api/auth/challenge-response', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ proof: bufferToBase64(proof), state }),
});
// 9. Redirect to AS for OIDC login (using the exact authorize_url from RP)
window.location.href = authorize_url;

9. Conclusion: A Secure Session is Established

Following this protocol, we have successfully transformed a standard bearer session cookie into a high-assurance, sender-constrained credential. The HttpOnly cookie is now cryptographically bound to the browser that initiated the login, neutralizing threats from a compromised server or stolen session IDs.

This mechanism aims to render session cookie stealing useless. However, it does not prevent session handover, where an attacker uses phishing techniques to have a legitimate user log them in. Such a risk can occur with credentials that are not phishing-resistant. This risk can be mitigated by using phishing resident frameworks such as WebAuthn, which leverages the FIDO2 and CTAP technologies to provide strong, origin-bound, phishing-resistant authentication.

In Part 2 of this series, we will build upon this foundation and explore how this secure session is used to authorize API requests, detailing the dual-mode patterns for providing DPoP proofs for every downstream resource call with key pair generated by browser.

10. References and Further Reading

Core Specifications

Implementation Technologies

Architectural Patterns & Security Guidance


메타데이터
post_id
b48431e1e908
slug
sender-constrained-dpop-jar-par-oidc-flow-browser-to-rp-full-technical-design-b48431e1e908
url
https://medium.com/@yveskerbs89/sender-constrained-dpop-jar-par-oidc-flow-browser-to-rp-full-technical-design-b48431e1e908
canonical_url
https://medium.com/@yveskerbs89/sender-constrained-dpop-jar-par-oidc-flow-browser-to-rp-full-technical-design-b48431e1e908
author_url
https://medium.com/@yveskerbs89
status
ok
fetched_at
2026-06-25 07:00:49