[forge-iterating] AB#3677516: Add internal DPoP crypto key storage and cache helpers#8708
Conversation
Add browser ES256 token-binding key provisioning, scoped DPoP key reuse, token-binding JWT signing seams, and supporting tests/docs/API review updates. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add beachball changefiles for browser, common, and node package changes in the DPoP key lifecycle PR. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds browser-side ES256 DPoP token-binding key lifecycle support (provision/reuse, public JWK lookup, compact JWT signing), and refactors SHR/PoP signing to use the same token-binding seams, with new telemetry fields and updated tests/docs/API reviews across msal-common, msal-browser, and msal-node.
Changes:
- Extend
ICryptowith token-binding key provisioning/lookup/signing APIs and add JOSE header helpers for SHR + DPoP. - Implement browser token-binding key caching/scoping + signing in
CryptoOps, and update PoP/DPoP generators/tests to use the new seams. - Add new error codes/docs and enrich performance telemetry with token-binding key metadata.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/msal-node/test/utils/TestConstants.ts | Extends node test crypto stub with new token-binding crypto surface (not implemented). |
| lib/msal-node/src/crypto/CryptoProvider.ts | Adds unimplemented node placeholders for token-binding provisioning/lookup/signing; deprecates signJwt. |
| lib/msal-node/apiReview/msal-node.api.md | Updates msal-node API review to include new ICrypto members and deprecation markers. |
| lib/msal-common/test/crypto/PopTokenGenerator.spec.ts | Updates SHR/PoP generator tests to validate token-binding provisioning and signing seams. |
| lib/msal-common/test/crypto/JoseHeader.spec.ts | Adds correlationId assertions for JOSE header error cases; adds DPoP header test coverage. |
| lib/msal-common/test/crypto/DpopProofGenerator.spec.ts | Refactors DPoP tests to use signTokenBindingJwt + key context instead of a custom signer. |
| lib/msal-common/test/client/ClientTestUtils.ts | Extends common test crypto mock with token-binding provisioning/lookup/signing methods. |
| lib/msal-common/src/utils/Constants.ts | Adds JsonWebTokenTypes.Dpop constant for DPoP JOSE headers. |
| lib/msal-common/src/telemetry/performance/PerformanceEvent.ts | Adds token-binding key telemetry metadata fields to performance events. |
| lib/msal-common/src/exports-common.ts | Exposes new crypto types/constants via common exports. |
| lib/msal-common/src/error/JoseHeaderErrorCodes.ts | Adds missing_jwk_error code for DPoP header validation. |
| lib/msal-common/src/crypto/PopTokenGenerator.ts | Updates SHR signing flow to provision token-binding key + use token-binding JWK/signing seams. |
| lib/msal-common/src/crypto/JoseHeader.ts | Adds jwk support and introduces typed builders for SHR and DPoP headers with correlationId propagation. |
| lib/msal-common/src/crypto/ICrypto.ts | Adds token-binding key provisioning/context types + new crypto interface methods; marks legacy helpers deprecated. |
| lib/msal-common/src/crypto/DpopProofGenerator.ts | Adds DPoP key scope + JKT generation and routes proof signing through signTokenBindingJwt. |
| lib/msal-common/apiReview/msal-common.api.md | Updates public API review snapshot for new crypto APIs, constants, and telemetry fields. |
| lib/msal-browser/test/crypto/CryptoOps.spec.ts | Adds/updates tests for scoped key reuse, signing behaviors, and token-binding telemetry emission. |
| lib/msal-browser/src/error/BrowserAuthErrorCodes.ts | Adds new browser error codes for token-binding JWT algorithm validation. |
| lib/msal-browser/src/crypto/CryptoOps.ts | Implements browser token-binding key lifecycle (scoped caching, signing, public JWK lookup) and telemetry. |
| lib/msal-browser/src/crypto/BrowserCrypto.ts | Broadens sign input type to BufferSource for WebCrypto API compatibility. |
| lib/msal-browser/apiReview/msal-browser.api.md | Updates browser API review snapshot for new error codes and JsonWebTokenTypes.Dpop. |
| docs/errors.md | Documents new JOSE header and browser token-binding-related error codes. |
Resolve the DPoP proof public JWK from the token-binding key used for signing instead of accepting caller-provided header key material. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move browser token-binding key provisioning, scoped lookup, public JWK export, removal, and key telemetry shaping into an internal TokenBindingKeyManager. Keep JWT signing in CryptoOps while removing new token-binding lifecycle methods from the public ICrypto contract. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a distinct browser error for supported JWT alg values that do not match the stored token-binding key material. Preserve caller-provided SHR alg values and fall back to RS256 when exported JWK alg metadata is missing. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ensure scoped DPoP provisioning can reuse persisted keys when memory contains unrelated entries, validates cached key metadata before reuse, and coalesces concurrent scoped provisioning requests. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify that legacy ICrypto and implementation signing/thumbprint helpers are SHR compatibility wrappers for MSAL-internal token-binding flows. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Treat an empty DPoP JOSE header JWK as an internal invariant failure while keeping algorithm-specific key policy outside the generic JOSE header helper. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the PR link required by changefile guidance to the DPoP key lifecycle changefiles. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document DPoP IndexedDB compatibility and add TSDoc for token-binding key manager surfaces. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use request-context DPoP key metadata for cache partitioning, keep runtime DPoP requests fail-closed, and share token-binding fallback storage across manager instances. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject token-binding JWT headers whose embedded JWK thumbprint does not match the signing key identifier. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move TokenBindingKeyManager lifecycle coverage into a focused spec while keeping CryptoOps proof/signing integration tests in CryptoOps.spec.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Coder
|
/azp run |
|
Azure Pipelines: Successfully started running 4 pipeline(s). |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 60 out of 60 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
lib/msal-common/src/crypto/ITokenBindingKeyManager.ts:83
- DEFAULT_TOKEN_BINDING_KEY_MANAGER always throws with an empty correlationId and omits the interface parameters, which drops the caller’s correlationId from the resulting ClientAuthError. Since these methods are invoked with a correlationId, the default implementation should accept/forward it so errors are properly correlated.
| idTokenClaims, | ||
| undefined, | ||
| undefined, | ||
| undefined, | ||
| this.config.tokenBindingKeyManager |
Use msal-common token-binding key manager types directly from browser and propagate correlation IDs through the default token-binding key manager. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| /** | ||
| * Get all the keys from the in-memory cache as an iterable array of strings. If no keys are found, query the keys in the | ||
| * asynchronous storage object. | ||
| * Get all keys from both in-memory and persistent storage. | ||
| * @param correlationId | ||
| */ | ||
| async getKeys(correlationId: string): Promise<string[]> { | ||
| const cacheKeys = this.inMemoryCache.getKeys(); | ||
| if (cacheKeys.length === 0) { | ||
| try { | ||
| this.logger.verbose( | ||
| "In-memory cache is empty, now querying persistent storage.", | ||
| correlationId | ||
| ); | ||
| return await this.indexedDBCache.getKeys(); | ||
| } catch (e) { | ||
| this.handleDatabaseAccessError(e, correlationId); | ||
| } | ||
| try { | ||
| const persistentCacheKeys = await this.indexedDBCache.getKeys(); | ||
| return Array.from(new Set([...cacheKeys, ...persistentCacheKeys])); | ||
| } catch (e) { | ||
| this.handleDatabaseAccessError(e, correlationId); | ||
| } | ||
|
|
||
| return cacheKeys; | ||
| } |
| throw createBrowserAuthError( | ||
| BrowserAuthErrorCodes.invalidPublicJwk, | ||
| correlationId, | ||
| BrowserAuthErrorCodes.tokenBindingKeyJwkThumbprintMismatch | ||
| ); |
Promote DPoP lifecycle changefiles to minor because ICrypto now exposes token binding hooks, add discoverable docs for new token-binding suberrors, and commit formatting updates from validation. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 61 out of 61 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
lib/msal-common/src/crypto/ICrypto.ts:148
- DEFAULT_CRYPTO_IMPLEMENTATION.removeTokenBindingKey throws a methodNotImplemented error with an empty correlationId. Since ICrypto now requires correlationId and callers rely on it for diagnostics, the default stub should accept the correlationId parameter (and optional context) and pass it through to createClientAuthError.
lib/msal-common/src/crypto/ICrypto.ts:160 - DEFAULT_CRYPTO_IMPLEMENTATION.signTokenBindingJwt throws methodNotImplemented with an empty correlationId and doesn't accept the required parameters. Update the stub signature to match ICrypto and pass the provided correlationId through to createClientAuthError for better diagnostics.
lib/msal-node/src/crypto/CryptoProvider.ts:81 - CryptoProvider.removeTokenBindingKey stub no longer matches the ICrypto signature (kid, correlationId, optional context). Because CryptoProvider is a public class, consumers calling it directly will see the wrong type signature. Align the method signature with ICrypto even if the node implementation remains unimplemented.
| async clearKeystore(): Promise<boolean> { | ||
| throw createClientAuthError( | ||
| ClientAuthErrorCodes.methodNotImplemented, | ||
| "" | ||
| ); |
| /** | ||
| * Removes all cryptographic keys from Keystore | ||
| */ | ||
| clearKeystore(): Promise<boolean> { | ||
| throw new Error("Method not implemented."); | ||
| } |
| /** | ||
| * Signs the given object as a jwt payload with private key retrieved by given kid - currently not implemented for node | ||
| * Signs a compact JWT with a token-binding key - not yet implemented for node | ||
| */ | ||
| signJwt(): Promise<string> { | ||
| signTokenBindingJwt(): Promise<string> { | ||
| throw new Error("Method not implemented."); | ||
| } |
Pass the configured token binding key manager into node ResponseHandler paths so PoP signing does not accidentally fall back to the default unsupported manager. Update the common PoP regression test to provide the mock token binding manager for signing. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
lib/msal-browser/src/cache/AsyncMemoryStorage.ts:106
AsyncMemoryStorage.getKeysnow always queries IndexedDB even when in-memory keys exist.DatabaseStorage.getKeys()can reject with a rawEvent(transaction error), andhandleDatabaseAccessErrorwill rethrow non-databaseUnavailableerrors, meaninggetKeyscan now throw instead of falling back to in-memory keys. This undermines the intended "persistent storage backed by memory" behavior and can cause unexpected failures in callers that only need the in-memory keys (e.g., key-scope lookups).
async getKeys(correlationId: string): Promise<string[]> {
const cacheKeys = this.inMemoryCache.getKeys();
try {
const persistentCacheKeys = await this.indexedDBCache.getKeys();
return Array.from(new Set([...cacheKeys, ...persistentCacheKeys]));
lib/msal-common/src/crypto/ICrypto.ts:101
ICryptois exported as part of the public@azure/msal-commonsurface, and this change removes/renames existing members (getPublicKeyThumbprint,signJwt) intosignTokenBindingJwtwith a different signature. Any external consumer providing a customcryptoInterfaceimplementation (or calling these methods on anICrypto-typed object) will experience a compile-time break, which is semver-major. Consider keeping the old members as deprecated (with a compatibility shim/fallback in internal call sites) for at least one major version before removing them, or otherwise ensure this lands under an intentional major-version plan.
Preserve caller correlation when DPoP header public JWK validation fails and when scoped token-binding key requests join an active provisioning operation. Accepted review context: JWK sanitization is intentionally not added because browser exports public JWKs from WebCrypto public keys, node token-binding is not implemented, and L1 does not support a custom key-manager surface. Static key storage using the first logger is accepted because operation-specific storage calls still receive the current correlation id. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
lib/msal-browser/src/crypto/CryptoOps.ts:322
- When the token-binding JWT header includes a
jwkproperty but it is not an object, this throwsinvalidPublicJwkwith subErrortoken_binding_key_jwk_thumbprint_mismatch. That subError is misleading here (no thumbprint comparison occurred) and makes diagnostics harder. Prefer throwinginvalidPublicJwkwithout the thumbprint-mismatch subError for this malformed-header case; keep the mismatch subError only for the actualkidvs JWK-thumbprint mismatch path invalidateTokenBindingJwtHeaderKey.
throw createBrowserAuthError(
BrowserAuthErrorCodes.invalidPublicJwk,
correlationId,
BrowserAuthErrorCodes.tokenBindingKeyJwkThumbprintMismatch
);
lib/msal-common/src/crypto/ICrypto.ts:161
DEFAULT_CRYPTO_IMPLEMENTATION.signTokenBindingJwtalways throwsmethodNotImplementedwith an empty correlationId (""), even though callers now must provide a correlationId tosignTokenBindingJwt. This drops useful diagnostics. Update the default stub to accept the method parameters and pass the provided correlationId through tocreateClientAuthError.
Use the normalized token-binding JWT header as the protected header input instead of reserializing the caller-owned object after async validation. Add regression coverage that mutates the original DPoP header during validation and verifies the signed header remains normalized. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve in-memory cache keys when IndexedDB key enumeration fails unexpectedly and memory fallback contains entries. Unexpected persistent failures still surface when there is no memory fallback. Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Developer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| const tokenBindingJwtHeader: TokenBindingSigningHeader = { | ||
| alg: header.alg, | ||
| }; | ||
|
|
||
| return signedJwt; | ||
| if (!("jwk" in header) || typeof header.jwk === "undefined") { |
| expect(proofHeader).toEqual({ | ||
| alg: "ES256", | ||
| jwk: publicJwk, | ||
| }); |
…-browser-es256-key-lifecycle
Replace ResponseHandler.generateAuthenticationResult optional positional parameters with a named options object so token-binding manager wiring does not rely on placeholder undefined arguments. Forge-Agent: Developer Co-authored-by: Forge <forge-bot@entra.github.io> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 67 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
lib/msal-browser/src/crypto/CryptoOps.ts:322
- When
header.jwkis present but not an object,validateTokenBindingSigningHeaderthrowsinvalid_public_jwkwith subErrortoken_binding_key_jwk_thumbprint_mismatch. That subError is misleading here (no thumbprint comparison happened), and will complicate diagnosing malformed headers.
throw createBrowserAuthError(
BrowserAuthErrorCodes.invalidPublicJwk,
correlationId,
BrowserAuthErrorCodes.tokenBindingKeyJwkThumbprintMismatch
);
| const keyPair: CryptoKeyPair = await BrowserCrypto.generateKeyPair( | ||
| false, | ||
| usages, | ||
| keyGenAlgorithm | ||
| ); | ||
| const publicJwk: JsonWebKey = await BrowserCrypto.exportJwk( | ||
| keyPair.publicKey | ||
| ); | ||
| const keyId = await BrowserCrypto.computeJwkThumbprint( | ||
| publicJwk, | ||
| correlationId | ||
| ); | ||
| return { | ||
| publicKey: keyPair.publicKey, | ||
| privateKey: keyPair.privateKey, | ||
| keyId, | ||
| }; |
Summary
Adds internal DPoP crypto, browser key lifecycle, and token-binding helper primitives for L1 DPoP while keeping app-facing DPoP acquisition disabled until the public PCA enablement work item.
Implements
Scope
ICryptoowns crypto/signing andITokenBindingKeyManagerowns key provisioning, removal, and public JWK lookup.TokenBindingKeyManagerthrough interaction/custom-auth clients so scoped provisioning dedupe is not lost across per-request clients.AuthenticationScheme.DPOPor enable app-facing DPoP flows.CryptoOps.spec.ts.Feature flag / waiver
Rollout / rollback
How to validate
npm test --workspace=@azure/msal-common -- JoseHeader.spec.ts JoseHeaderError.spec.ts DpopProofGenerator.spec.tsnpm test --workspace=@azure/msal-common -- --runTestsByPath test/crypto/PopTokenGenerator.spec.ts --runInBandnpm test --workspace=@azure/msal-browser -- CryptoOps.spec.ts TokenBindingKeyManager.spec.tsnpm run build:all,npm run lint,npm run format:check, andnpm run apiExtractor -- --localin affected 3P packagesbuild:allbeforeapiExtractor -- --local.Notes
This PR previously referenced removed work item AB#3674407. The active WI-2 tracking item is AB#3677516.