-
Notifications
You must be signed in to change notification settings - Fork 49
fix(core): include the RFC 9449 ath claim in DPoP proofs (closes #3184) #4292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |
| // | ||
|
|
||
| import type { JWK, CryptoKey } from "jose"; | ||
| import { SignJWT, generateKeyPair, exportJWK } from "jose"; | ||
| import { SignJWT, generateKeyPair, exportJWK, base64url } from "jose"; | ||
| import { v4 } from "uuid"; | ||
| import { PREFERRED_SIGNING_ALG } from "../constant"; | ||
|
|
||
|
|
@@ -41,23 +41,47 @@ export type KeyPair = { | |
| }; | ||
|
|
||
| /** | ||
| * Creates a DPoP header according to https://tools.ietf.org/html/draft-fett-oauth-dpop-04, | ||
| * Computes the RFC 9449 `ath` claim: the base64url-encoded SHA-256 hash of the | ||
| * ASCII encoding of the access token's value (RFC 9449 §4.2). Uses the WHATWG | ||
| * Web Crypto API (`crypto.subtle`) — the same cross-platform primitive `jose` | ||
| * relies on, available in browsers and the Node versions `jose` v6 supports. | ||
| * | ||
| * @hidden | ||
| */ | ||
| async function accessTokenHash(accessToken: string): Promise<string> { | ||
| const digest = await crypto.subtle.digest( | ||
| "SHA-256", | ||
| new TextEncoder().encode(accessToken), | ||
| ); | ||
| return base64url.encode(new Uint8Array(digest)); | ||
| } | ||
|
Comment on lines
+51
to
+57
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /** | ||
| * Creates a DPoP header according to {@link https://www.rfc-editor.org/rfc/rfc9449 RFC 9449}, | ||
| * based on the target URL and method, using the provided key. | ||
| * | ||
| * @param audience Target URL. | ||
| * @param method HTTP method allowed. | ||
| * @param dpopKey Key used to sign the token. | ||
| * @param accessToken When provided, the proof is bound to this access token via the | ||
| * RFC 9449 `ath` claim. This is REQUIRED (RFC 9449 §7) whenever the DPoP proof | ||
| * accompanies an access token to a protected resource — without it, a correctly | ||
| * enforcing resource server rejects the request with 401. | ||
| * @returns A JWT that can be used as a DPoP Authorization header. | ||
| */ | ||
| export async function createDpopHeader( | ||
| audience: string, | ||
| method: string, | ||
| dpopKey: KeyPair, | ||
| accessToken?: string, | ||
| ): Promise<string> { | ||
| return new SignJWT({ | ||
| htu: normalizeHTU(audience), | ||
| htm: method.toUpperCase(), | ||
| jti: v4(), | ||
| ...(accessToken !== undefined | ||
| ? { ath: await accessTokenHash(accessToken) } | ||
| : {}), | ||
| }) | ||
| .setProtectedHeader({ | ||
| alg: PREFERRED_SIGNING_ALG[0], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point on the encoding semantics. This is safe in practice because an OAuth 2.0 access token is ASCII-restricted: RFC 6750 §2.1 defines
b64token = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=", and JWT access tokens (RFC 9068) are dot-separated base64url — also ASCII. SoTextEncoder(UTF-8) and ASCII produce identical bytes for any valid token, matching howjose/oauth4webapicompute the same hash internally. Happy to add an explicit ASCII assertion if you'd prefer the defensive contract. (A follow-up migrates this proof-creation path to panva'sdpoppackage, which computesathinternally — removing this hand-rolled hash entirely.)