Sender-Constrained DPoP + JAR/PAR OIDC Flow (Browser to RP): Full Technical Design
1. Executive Summary
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. **stateParameter (RFC 6749):** Thestateparameter 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_jktquery 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:
**HttpOnlyFlag:** Prevents client-side scripts from accessing the cookie.**SecureFlag:** Ensures the cookie is only sent over HTTPS.**SameSiteFlag (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:
- 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.
- 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 thestateparameter of the OIDC authorization request. The Authorization Server (AS) treats thestateparameter 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
codeand the structuredstateparameter containing thedpop_jkt. The RP validates the pre-authenticationproof(if used) and exchanges thecodeto 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 thedpop_jktto 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
DPoPproofs 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
stateparameter to correlate two key events:
- Proof of Possession: The browser proves it owns the private key by solving a challenge sent by the RP.
- Proof of Authentication: The AS confirms the user’s identity and returns the same
statevalue 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
stateparameter. 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
- Browser: Generates a new DPoP keypair (e.g., ECDSA P-256 via Web Crypto API), ensuring the private key is
non-extractable. - Browser: Computes the JWK Thumbprint (
dpop_jkt) of the public key. - Browser:
POSTs its public JWK to a dedicated RP endpoint (e.g.,/api/auth/login). - RP: Receives the public JWK and
dpop_jkt. - RP: Generates a cryptographically secure random challenge (
nonce). - RP: Encrypts the
nonceusing the browser’s public key to create anenc_challenge. - RP: Generates a separate, cryptographically secure random value (
csrf_token) for thestateparameter's CSRF protection. - RP: Creates a structured
stateparameter by combining the CSRF token and the browser'sdpop_jkt.
- Example:
state="{csrf_token}:{dpop_jkt}" - The
statevalue 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.
- Example:
[https://as.example.com/authorize?request={JAR}&state={state}](https://as.example.com/authorize?request={JAR}&state={state})
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
- Browser: Validates the signed
JARby verifying theJARsignature using the RP’s public key. - Browser: Extracts the
dpop_jktfrom theJARand compares it to its own locally stored value. If any validation fails, the flow is aborted. - Browser: Decrypts the
enc_challenge(nonce) using its private DPoP key. - Browser: Signs the decrypted challenge to create a
proof. - Browser:
POSTs{ proof, state }to a dedicated RP endpoint (e.g.,/api/auth/challenge-response).
- The RP stores this
proof, associating it with thestate. - No session is issued yet.
C. OIDC Authentication and Session Issuance
- Browser: Redirects to the Authorization Server’s /authorize endpoint by navigating to the provided
authorize_url. - AS: Presents the authentication UI; the user logs in at the AS.
- AS: Redirects the browser back to the RP’s OIDC callback endpoint with an authorization code and the original state.
- 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 Tokenwith 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_jktin 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
- RFC 9449: OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)
- https://www.rfc-editor.org/rfc/rfc9449.html
- RFC 9101: JWT-Secured Authorization Request (JAR)
- https://www.rfc-editor.org/rfc/rfc9101.html
- FAPI 2.0: Financial-grade API Security Profile 2.0
- https://openid.net/specs/fapi-2_0-security-profile-ID2.html
Implementation Technologies
- W3C Web Cryptography API:
- https://www.w3.org/TR/WebCryptoAPI/
- RFC 7518: JSON Web Algorithms (JWA) — Defines the algorithms for JWK Thumbprint calculation and signatures
- https://www.rfc-editor.org/rfc/rfc7518.html
- ECIES (Elliptic Curve Integrated Encryption Scheme):
- https://datatracker.ietf.org/doc/html/draft-ietf-lwig-ecies-10
Architectural Patterns & Security Guidance
- OAuth 2.0 for Browser-Based Apps (Best Current Practice):
- https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps
- OWASP Session Management Cheat Sheet:
- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- Spring Security DPoP Documentation:
- https://docs.spring.io/spring-security/reference/servlet/oauth2/dpop.html
- Curity — The OAuth 2.0 Authorization Code Flow:
- https://curity.io/resources/learn/openid-code-flow/
메타데이터
- 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