@better-auth/core 1.7.0-beta.5 → 1.7.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/api/index.d.mts +44 -1
  2. package/dist/api/index.mjs +40 -1
  3. package/dist/context/global.mjs +1 -1
  4. package/dist/context/transaction.d.mts +7 -4
  5. package/dist/context/transaction.mjs +6 -3
  6. package/dist/db/adapter/factory.mjs +56 -30
  7. package/dist/db/adapter/index.d.mts +54 -10
  8. package/dist/db/adapter/types.d.mts +1 -1
  9. package/dist/db/type.d.mts +12 -7
  10. package/dist/instrumentation/tracer.mjs +1 -1
  11. package/dist/oauth2/dpop.d.mts +142 -0
  12. package/dist/oauth2/dpop.mjs +246 -0
  13. package/dist/oauth2/index.d.mts +3 -2
  14. package/dist/oauth2/index.mjs +3 -2
  15. package/dist/oauth2/verify.d.mts +74 -15
  16. package/dist/oauth2/verify.mjs +172 -20
  17. package/dist/social-providers/index.d.mts +1 -0
  18. package/dist/social-providers/microsoft-entra-id.d.mts +10 -0
  19. package/dist/social-providers/microsoft-entra-id.mjs +17 -2
  20. package/dist/social-providers/reddit.mjs +1 -1
  21. package/dist/social-providers/wechat.mjs +1 -1
  22. package/dist/types/context.d.mts +17 -0
  23. package/dist/types/init-options.d.mts +45 -5
  24. package/dist/types/plugin-client.d.mts +12 -2
  25. package/dist/utils/host.mjs +4 -0
  26. package/dist/utils/url.mjs +4 -3
  27. package/package.json +5 -5
  28. package/src/api/index.ts +82 -0
  29. package/src/context/transaction.ts +45 -12
  30. package/src/db/adapter/factory.ts +127 -72
  31. package/src/db/adapter/index.ts +54 -9
  32. package/src/db/adapter/types.ts +1 -0
  33. package/src/db/type.ts +12 -7
  34. package/src/oauth2/dpop.ts +568 -0
  35. package/src/oauth2/index.ts +44 -1
  36. package/src/oauth2/verify.ts +329 -66
  37. package/src/social-providers/microsoft-entra-id.ts +44 -1
  38. package/src/social-providers/reddit.ts +5 -1
  39. package/src/social-providers/wechat.ts +8 -1
  40. package/src/types/context.ts +18 -0
  41. package/src/types/init-options.ts +40 -8
  42. package/src/types/plugin-client.ts +16 -2
  43. package/src/utils/host.ts +15 -0
  44. package/src/utils/url.ts +10 -4
@@ -0,0 +1,246 @@
1
+ import { base64url, calculateJwkThumbprint, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
2
+ //#region src/oauth2/dpop.ts
3
+ const DPOP_AUTHORIZATION_SCHEME = "DPoP";
4
+ const BEARER_AUTHORIZATION_SCHEME = "Bearer";
5
+ const DPOP_PROOF_TYPE = "dpop+jwt";
6
+ const DPOP_SIGNING_ALGORITHMS = [
7
+ "EdDSA",
8
+ "ES256",
9
+ "ES512",
10
+ "PS256",
11
+ "RS256"
12
+ ];
13
+ const DEFAULT_DPOP_PROOF_MAX_AGE_SECONDS = 300;
14
+ const MAX_DPOP_JTI_LENGTH = 512;
15
+ const JWK_PRIVATE_FIELDS = new Set([
16
+ "d",
17
+ "p",
18
+ "q",
19
+ "dp",
20
+ "dq",
21
+ "qi",
22
+ "oth",
23
+ "k"
24
+ ]);
25
+ function createInMemoryDpopReplayStore() {
26
+ const reservations = /* @__PURE__ */ new Map();
27
+ return { reserve({ key, expiresAt, now }) {
28
+ const nowMs = now.getTime();
29
+ for (const [storedKey, expiresAtMs] of reservations) if (expiresAtMs <= nowMs) reservations.delete(storedKey);
30
+ if (reservations.has(key)) return false;
31
+ reservations.set(key, expiresAt.getTime());
32
+ return true;
33
+ } };
34
+ }
35
+ /**
36
+ * Database-backed DPoP proof replay store built on the auth context's
37
+ * verification reservation primitive (`internalAdapter.reserveVerificationValue`),
38
+ * the same atomic single-use mechanism that guards SAML assertion ids and other
39
+ * one-time tokens. A replayed proof collides on the deterministic reservation id
40
+ * so `reserve` returns `false`, giving cross-instance anti-replay. Prefer this
41
+ * over {@link createInMemoryDpopReplayStore} for any multi-instance or serverless
42
+ * resource server. Requires database-backed verification storage; a
43
+ * secondary-storage-only deployment rejects the proof (fails closed).
44
+ */
45
+ function createDpopReplayStore(reservations) {
46
+ return { reserve: ({ key, expiresAt }) => reservations.reserveVerificationValue({
47
+ identifier: `dpop-proof:${key}`,
48
+ value: key,
49
+ expiresAt
50
+ }) };
51
+ }
52
+ function createDpopProofError(code, message) {
53
+ return Object.assign(new Error(message), { code });
54
+ }
55
+ function isDpopProofError(error) {
56
+ return error instanceof Error && "code" in error && error.code === "invalid_dpop_proof";
57
+ }
58
+ function parseAccessTokenAuthorization(authorization) {
59
+ if (!authorization) return void 0;
60
+ const value = authorization.trim();
61
+ if (!value) return void 0;
62
+ const match = /^([A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*)\s+(.+)$/.exec(value);
63
+ if (!match) return {
64
+ scheme: "Unknown",
65
+ token: value
66
+ };
67
+ const scheme = match[1] ?? "";
68
+ const token = match[2]?.trim() ?? "";
69
+ if (scheme.toLowerCase() === "bearer") return {
70
+ scheme: "Bearer",
71
+ token
72
+ };
73
+ if (scheme.toLowerCase() === "dpop") return {
74
+ scheme: "DPoP",
75
+ token
76
+ };
77
+ return {
78
+ scheme: "Unknown",
79
+ token: value
80
+ };
81
+ }
82
+ function stripAccessTokenAuthorizationScheme(token) {
83
+ return parseAccessTokenAuthorization(token)?.token ?? token;
84
+ }
85
+ function normalizeDpopHtu(url) {
86
+ const parsed = new URL(url);
87
+ if (parsed.hash) throw new Error("DPoP proof htu must not contain a fragment");
88
+ return `${parsed.origin}${parsed.pathname}`;
89
+ }
90
+ async function deriveDpopAth(accessToken) {
91
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(accessToken));
92
+ return base64url.encode(new Uint8Array(digest));
93
+ }
94
+ async function deriveDpopJkt(jwk) {
95
+ return calculateJwkThumbprint(jwk, "sha256");
96
+ }
97
+ /**
98
+ * Extracts the DPoP key thumbprint from an RFC 7800 `cnf` confirmation. The
99
+ * input is untrusted (a JWT claim, a JSON column), so any shape other than an
100
+ * object carrying a non-empty string `jkt` (a primitive, an array, a different
101
+ * confirmation method such as mTLS `x5t#S256`) yields `undefined` instead of
102
+ * throwing.
103
+ */
104
+ function getConfirmationJkt(confirmation) {
105
+ if (!confirmation || typeof confirmation !== "object" || Array.isArray(confirmation)) return;
106
+ const jkt = confirmation.jkt;
107
+ return typeof jkt === "string" && jkt.length > 0 ? jkt : void 0;
108
+ }
109
+ function getDpopJktFromPayload(payload) {
110
+ return getConfirmationJkt(payload.cnf);
111
+ }
112
+ function getStringClaim(payload, claim) {
113
+ const value = payload[claim];
114
+ return typeof value === "string" && value.length > 0 ? value : void 0;
115
+ }
116
+ function getNumberClaim(payload, claim) {
117
+ const value = payload[claim];
118
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
119
+ }
120
+ function assertSupportedDpopAlgorithm(alg, signingAlgorithms) {
121
+ if (!alg || alg === "none" || alg.startsWith("HS")) throw createDpopProofError("invalid_dpop_proof", "DPoP proof must use an asymmetric JWS algorithm");
122
+ if (!signingAlgorithms.includes(alg)) throw createDpopProofError("invalid_dpop_proof", "DPoP proof uses an unsupported JWS algorithm");
123
+ }
124
+ function assertPublicJwk(jwk) {
125
+ if (!jwk || typeof jwk !== "object" || Array.isArray(jwk)) throw createDpopProofError("invalid_dpop_proof", "DPoP proof header must include a public jwk");
126
+ if (jwk.kty === "oct") throw createDpopProofError("invalid_dpop_proof", "DPoP proof jwk must be asymmetric");
127
+ for (const field of JWK_PRIVATE_FIELDS) if (field in jwk) throw createDpopProofError("invalid_dpop_proof", "DPoP proof jwk must not contain private key material");
128
+ }
129
+ async function deriveDpopReplayKey(params) {
130
+ const input = `${params.jkt}\n${params.htm}\n${params.htu}\n${params.jti}`;
131
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
132
+ return base64url.encode(new Uint8Array(digest));
133
+ }
134
+ async function reserveDpopReplay(replayStore, reservation) {
135
+ if (!replayStore) return;
136
+ if (!await replayStore.reserve(reservation)) throw createDpopProofError("invalid_dpop_proof", "DPoP proof jti has already been used");
137
+ }
138
+ async function verifyDpopProof({ proofJwt, method, url, accessToken, expectedJkt, requireAth = false, nowSeconds = Math.floor(Date.now() / 1e3), proofMaxAgeSeconds = DEFAULT_DPOP_PROOF_MAX_AGE_SECONDS, signingAlgorithms = DPOP_SIGNING_ALGORITHMS, replayStore }) {
139
+ if (!proofJwt || proofJwt.split(".").length !== 3) throw createDpopProofError("invalid_dpop_proof", "DPoP proof must be a compact JWT");
140
+ let protectedHeader;
141
+ try {
142
+ protectedHeader = decodeProtectedHeader(proofJwt);
143
+ } catch (error) {
144
+ throw createDpopProofError("invalid_dpop_proof", error instanceof Error ? error.message : "DPoP proof header is invalid");
145
+ }
146
+ if (protectedHeader.typ !== "dpop+jwt") throw createDpopProofError("invalid_dpop_proof", "DPoP proof typ must be \"dpop+jwt\"");
147
+ assertSupportedDpopAlgorithm(protectedHeader.alg, signingAlgorithms);
148
+ assertPublicJwk(protectedHeader.jwk);
149
+ let payload;
150
+ try {
151
+ payload = (await jwtVerify(proofJwt, await importJWK(protectedHeader.jwk, protectedHeader.alg), { typ: DPOP_PROOF_TYPE })).payload;
152
+ } catch (error) {
153
+ throw createDpopProofError("invalid_dpop_proof", error instanceof Error ? error.message : "DPoP proof signature is invalid");
154
+ }
155
+ const htm = getStringClaim(payload, "htm");
156
+ const htu = getStringClaim(payload, "htu");
157
+ const jti = getStringClaim(payload, "jti");
158
+ const iat = getNumberClaim(payload, "iat");
159
+ if (!htm || !htu || !jti || iat === void 0) throw createDpopProofError("invalid_dpop_proof", "DPoP proof must include htm, htu, jti, and iat claims");
160
+ if (jti.length > MAX_DPOP_JTI_LENGTH) throw createDpopProofError("invalid_dpop_proof", "DPoP proof jti is too large");
161
+ if (htm.toUpperCase() !== method.toUpperCase()) throw createDpopProofError("invalid_dpop_proof", "DPoP proof htm does not match the request method");
162
+ let normalizedHtu;
163
+ let proofHtu;
164
+ try {
165
+ normalizedHtu = normalizeDpopHtu(url);
166
+ proofHtu = normalizeDpopHtu(htu);
167
+ } catch (error) {
168
+ throw createDpopProofError("invalid_dpop_proof", error instanceof Error ? error.message : "DPoP proof htu is invalid");
169
+ }
170
+ if (proofHtu !== normalizedHtu) throw createDpopProofError("invalid_dpop_proof", "DPoP proof htu does not match the request URL");
171
+ if (iat > nowSeconds + 5 || nowSeconds - iat > proofMaxAgeSeconds) throw createDpopProofError("invalid_dpop_proof", "DPoP proof iat is outside the accepted window");
172
+ const ath = getStringClaim(payload, "ath");
173
+ if (requireAth && !ath) throw createDpopProofError("invalid_dpop_proof", "DPoP proof must include an ath claim");
174
+ if (accessToken !== void 0) {
175
+ if (ath !== await deriveDpopAth(accessToken)) throw createDpopProofError("invalid_dpop_proof", "DPoP proof ath does not match the access token");
176
+ }
177
+ const jkt = await deriveDpopJkt(protectedHeader.jwk);
178
+ if (expectedJkt !== void 0 && jkt !== expectedJkt) throw createDpopProofError("invalid_dpop_proof", "DPoP proof key does not match the bound token");
179
+ const replayKey = await deriveDpopReplayKey({
180
+ jkt,
181
+ htm: htm.toUpperCase(),
182
+ htu: normalizedHtu,
183
+ jti
184
+ });
185
+ const expiresAt = /* @__PURE__ */ new Date((iat + proofMaxAgeSeconds) * 1e3);
186
+ await reserveDpopReplay(replayStore, {
187
+ key: replayKey,
188
+ expiresAt,
189
+ now: /* @__PURE__ */ new Date(nowSeconds * 1e3)
190
+ });
191
+ return {
192
+ jwk: protectedHeader.jwk,
193
+ jkt,
194
+ jti,
195
+ htm,
196
+ htu: normalizedHtu,
197
+ iat,
198
+ ath,
199
+ replayKey,
200
+ expiresAt
201
+ };
202
+ }
203
+ function createDpopBindingError(code, message) {
204
+ return Object.assign(new Error(message), { code });
205
+ }
206
+ function isDpopBindingError(error) {
207
+ return error instanceof Error && "code" in error && (error.code === "invalid_token" || error.code === "invalid_dpop_proof");
208
+ }
209
+ /**
210
+ * Enforces the RFC 9449 §7.1 sender-constraint check for a resource request,
211
+ * given an access-token payload that has already been validated (by JWKS or
212
+ * introspection). This is the single source of truth for the
213
+ * "is the token DPoP-bound? then require the DPoP scheme, a proof, and a
214
+ * matching key" decision, shared by every resource-server entry point.
215
+ *
216
+ * Throws a {@link DpopBindingError} on any mismatch so callers map the
217
+ * `invalid_token` / `invalid_dpop_proof` code into their own transport. Returns
218
+ * normally for a valid bearer token (no `cnf.jkt`, no DPoP scheme).
219
+ */
220
+ async function enforceDpopBinding({ payload, authorization, proofJwt, method, url, replayStore, proofMaxAgeSeconds, signingAlgorithms }) {
221
+ const dpopJkt = getDpopJktFromPayload(payload);
222
+ if (!dpopJkt) {
223
+ if (authorization.scheme === "DPoP") throw createDpopBindingError("invalid_token", "DPoP authorization requires a DPoP-bound access token");
224
+ return;
225
+ }
226
+ if (authorization.scheme !== "DPoP") throw createDpopBindingError("invalid_token", "DPoP-bound access token requires the DPoP authorization scheme");
227
+ if (!proofJwt) throw createDpopBindingError("invalid_dpop_proof", "DPoP proof header is required");
228
+ try {
229
+ await verifyDpopProof({
230
+ proofJwt,
231
+ method,
232
+ url,
233
+ accessToken: authorization.token,
234
+ expectedJkt: dpopJkt,
235
+ requireAth: true,
236
+ proofMaxAgeSeconds,
237
+ signingAlgorithms,
238
+ replayStore
239
+ });
240
+ } catch (error) {
241
+ if (isDpopProofError(error)) throw createDpopBindingError("invalid_dpop_proof", error.message);
242
+ throw error;
243
+ }
244
+ }
245
+ //#endregion
246
+ export { BEARER_AUTHORIZATION_SCHEME, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, deriveDpopAth, deriveDpopJkt, enforceDpopBinding, getConfirmationJkt, getDpopJktFromPayload, isDpopBindingError, isDpopProofError, normalizeDpopHtu, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof };
@@ -5,10 +5,11 @@ import { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, O
5
5
  import { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
6
6
  import { clientCredentialsToken, clientCredentialsTokenRequest } from "./client-credentials-token.mjs";
7
7
  import { RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, createAuthorizationURL } from "./create-authorization-url.mjs";
8
+ import { AccessTokenAuthorization, AccessTokenAuthorizationScheme, BEARER_AUTHORIZATION_SCHEME, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, DpopBindingError, DpopBindingErrorCode, DpopProofError, DpopProofErrorCode, DpopReplayReservation, DpopReplayReservations, DpopReplayStore, DpopSigningAlgorithm, EnforceDpopBindingParams, VerifiedDpopProof, VerifyDpopProofOptions, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, deriveDpopAth, deriveDpopJkt, enforceDpopBinding, getConfirmationJkt, getDpopJktFromPayload, isDpopBindingError, isDpopProofError, normalizeDpopHtu, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "./dpop.mjs";
8
9
  import { refreshAccessToken, refreshAccessTokenRequest } from "./refresh-access-token.mjs";
9
10
  import { includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, resolveRequestedScopes, unionGrantedScopes } from "./scopes.mjs";
10
11
  import { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
11
12
  import { authorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
12
- import { getJwks, verifyAccessToken, verifyJwsAccessToken } from "./verify.mjs";
13
+ import { ResourceRequestInput, VerifyAccessTokenOptions, VerifyAccessTokenRequestOptions, getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken } from "./verify.mjs";
13
14
  import { supportsIdTokenSignIn, verifyProviderIdToken } from "./verify-id-token.mjs";
14
- export { type AuthorizationURLResult, CLIENT_ASSERTION_TYPE, type ClientAssertionContext, type ClientAssertionGetter, type ClientAssertionGrantType, type GrantAuthority, type OAuth2Tokens, type OAuth2UserInfo, type OAuthIdTokenConfig, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, type PrivateKeyJwtClientAssertionGetterOptions, type PrivateKeyJwtSigningAlgorithm, type ProviderGrantAuthority, type ProviderOptions, RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, type TokenEndpointAuth, type TokenEndpointAuthMethod, type TokenEndpointSecretAuthentication, type UpstreamProvider, additionalAuthorizationParamsSchema, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, decodeBasicCredentials, encodeBasicCredentials, generateCodeChallenge, getJwks, getOAuth2Tokens, getPrimaryClientId, includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, refreshAccessToken, refreshAccessTokenRequest, resolveClientAssertionParams, resolveRequestedScopes, signPrivateKeyJwtClientAssertion, supportsIdTokenSignIn, unionGrantedScopes, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken, verifyProviderIdToken };
15
+ export { type AccessTokenAuthorization, type AccessTokenAuthorizationScheme, type AuthorizationURLResult, BEARER_AUTHORIZATION_SCHEME, CLIENT_ASSERTION_TYPE, type ClientAssertionContext, type ClientAssertionGetter, type ClientAssertionGrantType, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, type DpopBindingError, type DpopBindingErrorCode, type DpopProofError, type DpopProofErrorCode, type DpopReplayReservation, type DpopReplayReservations, type DpopReplayStore, type DpopSigningAlgorithm, type EnforceDpopBindingParams, type GrantAuthority, type OAuth2Tokens, type OAuth2UserInfo, type OAuthIdTokenConfig, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, type PrivateKeyJwtClientAssertionGetterOptions, type PrivateKeyJwtSigningAlgorithm, type ProviderGrantAuthority, type ProviderOptions, RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, type ResourceRequestInput, type TokenEndpointAuth, type TokenEndpointAuthMethod, type TokenEndpointSecretAuthentication, type UpstreamProvider, type VerifiedDpopProof, type VerifyAccessTokenOptions, type VerifyAccessTokenRequestOptions, type VerifyDpopProofOptions, additionalAuthorizationParamsSchema, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationURL, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, createPrivateKeyJwtClientAssertionGetter, decodeBasicCredentials, deriveDpopAth, deriveDpopJkt, encodeBasicCredentials, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, getOAuth2Tokens, getPrimaryClientId, includesGrantedScope, isDpopBindingError, isDpopProofError, normalizeDpopHtu, normalizeScopes, parseAccessTokenAuthorization, parseScopeField, readGrantedScopes, refreshAccessToken, refreshAccessTokenRequest, requestToResourceInput, resolveClientAssertionParams, resolveRequestedScopes, signPrivateKeyJwtClientAssertion, stripAccessTokenAuthorizationScheme, supportsIdTokenSignIn, unionGrantedScopes, validateAuthorizationCode, validateToken, verifyAccessTokenRequest, verifyBearerToken, verifyDpopProof, verifyJwsAccessToken, verifyProviderIdToken };
@@ -5,8 +5,9 @@ import { additionalAuthorizationParamsSchema } from "./authorization-params.mjs"
5
5
  import { decodeBasicCredentials, encodeBasicCredentials } from "./basic-credentials.mjs";
6
6
  import { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, createPrivateKeyJwtClientAssertionGetter, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion } from "./client-assertion.mjs";
7
7
  import { clientCredentialsToken, clientCredentialsTokenRequest } from "./client-credentials-token.mjs";
8
+ import { BEARER_AUTHORIZATION_SCHEME, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, deriveDpopAth, deriveDpopJkt, enforceDpopBinding, getConfirmationJkt, getDpopJktFromPayload, isDpopBindingError, isDpopProofError, normalizeDpopHtu, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "./dpop.mjs";
8
9
  import { refreshAccessToken, refreshAccessTokenRequest } from "./refresh-access-token.mjs";
9
10
  import { authorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
10
- import { getJwks, verifyAccessToken, verifyJwsAccessToken } from "./verify.mjs";
11
+ import { getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken } from "./verify.mjs";
11
12
  import { supportsIdTokenSignIn, verifyProviderIdToken } from "./verify-id-token.mjs";
12
- export { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, additionalAuthorizationParamsSchema, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, decodeBasicCredentials, encodeBasicCredentials, generateCodeChallenge, getJwks, getOAuth2Tokens, getPrimaryClientId, includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, refreshAccessToken, refreshAccessTokenRequest, resolveClientAssertionParams, resolveRequestedScopes, signPrivateKeyJwtClientAssertion, supportsIdTokenSignIn, unionGrantedScopes, validateAuthorizationCode, validateToken, verifyAccessToken, verifyJwsAccessToken, verifyProviderIdToken };
13
+ export { BEARER_AUTHORIZATION_SCHEME, CLIENT_ASSERTION_TYPE, DPOP_AUTHORIZATION_SCHEME, DPOP_PROOF_TYPE, DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, additionalAuthorizationParamsSchema, applyDefaultAccessTokenExpiry, authorizationCodeRequest, clientCredentialsToken, clientCredentialsTokenRequest, createAuthorizationURL, createDpopBindingError, createDpopProofError, createDpopReplayStore, createInMemoryDpopReplayStore, createPrivateKeyJwtClientAssertionGetter, decodeBasicCredentials, deriveDpopAth, deriveDpopJkt, encodeBasicCredentials, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, getOAuth2Tokens, getPrimaryClientId, includesGrantedScope, isDpopBindingError, isDpopProofError, normalizeDpopHtu, normalizeScopes, parseAccessTokenAuthorization, parseScopeField, readGrantedScopes, refreshAccessToken, refreshAccessTokenRequest, requestToResourceInput, resolveClientAssertionParams, resolveRequestedScopes, signPrivateKeyJwtClientAssertion, stripAccessTokenAuthorizationScheme, supportsIdTokenSignIn, unionGrantedScopes, validateAuthorizationCode, validateToken, verifyAccessTokenRequest, verifyBearerToken, verifyDpopProof, verifyJwsAccessToken, verifyProviderIdToken };
@@ -1,6 +1,19 @@
1
+ import { DpopReplayStore } from "./dpop.mjs";
1
2
  import { JSONWebKeySet, JWTPayload, JWTVerifyOptions } from "jose";
2
3
 
3
4
  //#region src/oauth2/verify.d.ts
5
+ type JwksFetchOptions = {
6
+ /** Jwks url or promise of a Jwks */jwksFetch: string | (() => Promise<JSONWebKeySet | undefined>);
7
+ /**
8
+ * Stable object to cache the result of a function `jwksFetch` under,
9
+ * with the same TTL and kid-miss refetch rules as string sources.
10
+ * Without it, a function source is fetched on every verification.
11
+ */
12
+ jwksCacheKey?: object;
13
+ };
14
+ /**
15
+ * @internal
16
+ */
4
17
  interface VerifyAccessTokenRemote {
5
18
  /** Full url of the introspect endpoint. Should end with `/oauth2/introspect` */
6
19
  introspectUrl: string;
@@ -29,28 +42,74 @@ interface VerifyAccessTokenRemote {
29
42
  */
30
43
  allowMissingAudience?: boolean;
31
44
  }
45
+ interface VerifyAccessTokenOptions {
46
+ /** Verify options */
47
+ verifyOptions: JWTVerifyOptions & Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
48
+ /** Scopes to additionally verify. Token must include all but not exact. */
49
+ scopes?: string[];
50
+ /** Required to verify access token locally */
51
+ jwksUrl?: string;
52
+ /** If provided, can verify a token remotely */
53
+ remoteVerify?: VerifyAccessTokenRemote;
54
+ }
55
+ interface VerifyAccessTokenRequestOptions extends VerifyAccessTokenOptions {
56
+ dpop?: {
57
+ proofMaxAgeSeconds?: number;
58
+ /**
59
+ * Store used to reject replayed DPoP proof `jti` values.
60
+ *
61
+ * Defaults to a process-local in-memory store, which is only safe for a
62
+ * single-instance deployment: it shares no state across instances and
63
+ * resets on cold start, so a captured proof can be replayed against
64
+ * another instance within the proof's lifetime. Supply a shared,
65
+ * persistent store (for example one backed by your database) for any
66
+ * multi-instance or serverless resource server.
67
+ */
68
+ replayStore?: DpopReplayStore;
69
+ signingAlgorithms?: readonly string[];
70
+ };
71
+ }
72
+ interface ResourceRequestInput {
73
+ authorizationHeader: string | null | undefined;
74
+ dpopProofJwt?: string | null | undefined;
75
+ method: string;
76
+ url: string;
77
+ }
78
+ /**
79
+ * Builds a {@link ResourceRequestInput} from a standard `Request`, reading the
80
+ * `Authorization` and `DPoP` headers and the request method and URL. Resource
81
+ * servers share this so every entry point maps the wire request the same way.
82
+ */
83
+ declare function requestToResourceInput(request: Request): ResourceRequestInput;
32
84
  /**
33
85
  * Performs local verification of an access token for your APIs.
34
86
  *
35
87
  * Can also be configured for remote verification.
36
88
  */
37
- declare function verifyJwsAccessToken(token: string, opts: {
38
- /** Jwks url or promise of a Jwks */jwksFetch: string | (() => Promise<JSONWebKeySet | undefined>); /** Verify options */
39
- verifyOptions: JWTVerifyOptions & Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
89
+ declare function verifyJwsAccessToken(token: string, opts: JwksFetchOptions & {
90
+ /** Verify options */verifyOptions: JWTVerifyOptions & Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
40
91
  }): Promise<JWTPayload>;
41
- declare function getJwks(token: string, opts: {
42
- /** Jwks url or promise of a Jwks */jwksFetch: string | (() => Promise<JSONWebKeySet | undefined>);
43
- }): Promise<JSONWebKeySet>;
92
+ declare function getJwks(token: string, opts: JwksFetchOptions): Promise<JSONWebKeySet>;
44
93
  /**
45
- * Performs local verification of an access token for your API.
94
+ * Performs local verification of a bearer access token for your API.
46
95
  *
47
- * Can also be configured for remote verification.
96
+ * Can also be configured for remote verification. DPoP-bound access tokens
97
+ * require {@link verifyAccessTokenRequest}, because sender-constraining cannot
98
+ * be verified without the HTTP method, URL, Authorization scheme, DPoP proof,
99
+ * and access-token hash. This function rejects DPoP-bound tokens; reach for it
100
+ * only when you hold a raw token string and intentionally accept bearer tokens
101
+ * alone.
48
102
  */
49
- declare function verifyAccessToken(token: string, opts: {
50
- /** Verify options */verifyOptions: JWTVerifyOptions & Required<Pick<JWTVerifyOptions, "audience" | "issuer">>; /** Scopes to additionally verify. Token must include all but not exact. */
51
- scopes?: string[]; /** Required to verify access token locally */
52
- jwksUrl?: string; /** If provided, can verify a token remotely */
53
- remoteVerify?: VerifyAccessTokenRemote;
54
- }): Promise<JWTPayload>;
103
+ declare function verifyBearerToken(token: string, opts: VerifyAccessTokenOptions): Promise<JWTPayload>;
104
+ /**
105
+ * Verifies an HTTP resource request carrying an OAuth access token. This is the
106
+ * recommended resource-server entry point: it handles both bearer and
107
+ * DPoP-bound tokens, the bearer case being the request with no DPoP proof.
108
+ *
109
+ * It performs the same token validation as {@link verifyBearerToken}, then adds
110
+ * the RFC 9449 sender-constraint checks that need request context: authorization
111
+ * scheme, method, URL, DPoP proof, `ath`, and `cnf.jkt` binding.
112
+ */
113
+ declare function verifyAccessTokenRequest(request: ResourceRequestInput, opts: VerifyAccessTokenRequestOptions): Promise<JWTPayload>;
55
114
  //#endregion
56
- export { getJwks, verifyAccessToken, verifyJwsAccessToken };
115
+ export { ResourceRequestInput, VerifyAccessTokenOptions, VerifyAccessTokenRequestOptions, getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken };
@@ -1,4 +1,5 @@
1
1
  import { logger } from "../env/logger.mjs";
2
+ import { createInMemoryDpopReplayStore, enforceDpopBinding, getDpopJktFromPayload, isDpopBindingError, parseAccessTokenAuthorization } from "./dpop.mjs";
2
3
  import { APIError } from "better-call";
3
4
  import { UnsecuredJWT, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
4
5
  import { betterFetch } from "@better-fetch/fetch";
@@ -11,13 +12,65 @@ const joseInfrastructureErrorCodes = new Set([
11
12
  function isJoseInfrastructureError(error) {
12
13
  return joseInfrastructureErrorCodes.has(error.code);
13
14
  }
15
+ /**
16
+ * @internal
17
+ */
14
18
  const jwksCache = /* @__PURE__ */ new Map();
15
19
  /**
20
+ * Cache for function jwks sources, keyed by a caller-provided stable object.
21
+ * Entries are released with their key, so per-request keys cannot accumulate.
22
+ */
23
+ const functionJwksCache = /* @__PURE__ */ new WeakMap();
24
+ /**
16
25
  * How long a cached JWKS is trusted before it is refetched
17
26
  *
18
27
  * @internal
19
28
  */
20
29
  const JWKS_CACHE_TTL_MS = 300 * 1e3;
30
+ const JWKS_NO_KID_REFETCH_COOLDOWN_MS = 30 * 1e3;
31
+ /**
32
+ * Returns the cached key set when it is within the TTL. When the token carries
33
+ * `kid`, the cached set must contain that key id; without `kid`, key selection
34
+ * is deferred to JOSE because RFC 7515 makes the header parameter optional.
35
+ */
36
+ function getFreshJwksWithKid(cached, kid) {
37
+ if (!cached) return void 0;
38
+ if (Date.now() - cached.fetchedAt >= JWKS_CACHE_TTL_MS) return void 0;
39
+ if (kid && !cached.jwks.keys.some((jwk) => jwk.kid === kid)) return;
40
+ return cached.jwks;
41
+ }
42
+ function shouldRefetchCachedJwksWithoutKid(error, resolved) {
43
+ if (!(resolved.fromCache && !resolved.kid && (error instanceof errors.JWKSNoMatchingKey || error instanceof errors.JWSSignatureVerificationFailed))) return false;
44
+ if (!resolved.noKidRefetchedAt) return true;
45
+ return Date.now() - resolved.noKidRefetchedAt >= JWKS_NO_KID_REFETCH_COOLDOWN_MS;
46
+ }
47
+ async function fetchJwks(jwksFetch) {
48
+ const jwks = typeof jwksFetch === "string" ? await betterFetch(jwksFetch, { headers: { Accept: "application/json" } }).then(async (res) => {
49
+ if (res.error) throw new Error(`Jwks failed: ${res.error.message ?? res.error.statusText}`);
50
+ return res.data;
51
+ }) : await jwksFetch();
52
+ if (!jwks) throw new Error("No jwks found");
53
+ return jwks;
54
+ }
55
+ /**
56
+ * Builds a {@link ResourceRequestInput} from a standard `Request`, reading the
57
+ * `Authorization` and `DPoP` headers and the request method and URL. Resource
58
+ * servers share this so every entry point maps the wire request the same way.
59
+ */
60
+ function requestToResourceInput(request) {
61
+ return {
62
+ authorizationHeader: request.headers.get("authorization"),
63
+ dpopProofJwt: request.headers.get("dpop"),
64
+ method: request.method,
65
+ url: request.url
66
+ };
67
+ }
68
+ /**
69
+ * Process-local, single-instance replay store. See the warning on
70
+ * {@link VerifyAccessTokenRequestOptions.dpop.replayStore}; multi-instance
71
+ * resource servers must pass their own shared store.
72
+ */
73
+ const defaultDpopReplayStore = createInMemoryDpopReplayStore();
21
74
  /**
22
75
  * Performs local verification of an access token for your APIs.
23
76
  *
@@ -25,7 +78,17 @@ const JWKS_CACHE_TTL_MS = 300 * 1e3;
25
78
  */
26
79
  async function verifyJwsAccessToken(token, opts) {
27
80
  try {
28
- const jwt = await jwtVerify(token, createLocalJWKSet(await getJwks(token, opts)), opts.verifyOptions);
81
+ const resolved = await getJwksForVerification(token, opts);
82
+ let jwt;
83
+ try {
84
+ jwt = await jwtVerify(token, createLocalJWKSet(resolved.jwks), opts.verifyOptions);
85
+ } catch (error) {
86
+ if (shouldRefetchCachedJwksWithoutKid(error, resolved)) jwt = await jwtVerify(token, createLocalJWKSet((await getJwksForVerification(token, {
87
+ ...opts,
88
+ forceRefresh: true
89
+ })).jwks), opts.verifyOptions);
90
+ else throw error;
91
+ }
29
92
  if (jwt.payload.azp) jwt.payload.client_id = jwt.payload.azp;
30
93
  return jwt.payload;
31
94
  } catch (error) {
@@ -34,6 +97,9 @@ async function verifyJwsAccessToken(token, opts) {
34
97
  }
35
98
  }
36
99
  async function getJwks(token, opts) {
100
+ return (await getJwksForVerification(token, opts)).jwks;
101
+ }
102
+ async function getJwksForVerification(token, opts) {
37
103
  let jwtHeaders;
38
104
  try {
39
105
  jwtHeaders = decodeProtectedHeader(token);
@@ -41,32 +107,65 @@ async function getJwks(token, opts) {
41
107
  if (error instanceof Error) throw error;
42
108
  throw new Error(error);
43
109
  }
44
- if (!jwtHeaders.kid) throw new APIError("UNAUTHORIZED", { message: "invalid access token" });
45
110
  const kid = jwtHeaders.kid;
111
+ if (typeof opts.jwksFetch !== "string") {
112
+ const cacheKey = opts.jwksCacheKey;
113
+ if (!cacheKey) {
114
+ const jwks = await opts.jwksFetch();
115
+ if (!jwks) throw new Error("No jwks found");
116
+ return {
117
+ jwks,
118
+ fromCache: false,
119
+ kid
120
+ };
121
+ }
122
+ const cached = functionJwksCache.get(cacheKey);
123
+ const cachedJwks = opts.forceRefresh ? void 0 : getFreshJwksWithKid(cached, kid);
124
+ if (cachedJwks) return {
125
+ jwks: cachedJwks,
126
+ fromCache: true,
127
+ kid,
128
+ noKidRefetchedAt: cached?.noKidRefetchedAt
129
+ };
130
+ const jwks = await opts.jwksFetch();
131
+ if (!jwks) throw new Error("No jwks found");
132
+ const fetchedAt = Date.now();
133
+ functionJwksCache.set(cacheKey, {
134
+ jwks,
135
+ fetchedAt,
136
+ ...opts.forceRefresh && !kid ? { noKidRefetchedAt: fetchedAt } : {}
137
+ });
138
+ return {
139
+ jwks,
140
+ fromCache: false,
141
+ kid
142
+ };
143
+ }
46
144
  const cacheKey = opts.jwksFetch;
47
145
  const cached = jwksCache.get(cacheKey);
48
- const isFresh = cached ? Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS : false;
49
- const hasKid = cached?.jwks.keys.some((jwk) => jwk.kid === kid) ?? false;
50
- if (!cached || !isFresh || !hasKid) {
51
- const jwks = typeof opts.jwksFetch === "string" ? await betterFetch(opts.jwksFetch, { headers: { Accept: "application/json" } }).then(async (res) => {
52
- if (res.error) throw new Error(`Jwks failed: ${res.error.message ?? res.error.statusText}`);
53
- return res.data;
54
- }) : await opts.jwksFetch();
55
- if (!jwks) throw new Error("No jwks found");
146
+ const cachedJwks = opts.forceRefresh ? void 0 : getFreshJwksWithKid(cached, kid);
147
+ if (!cachedJwks) {
148
+ const jwks = await fetchJwks(opts.jwksFetch);
149
+ const fetchedAt = Date.now();
56
150
  jwksCache.set(cacheKey, {
57
151
  jwks,
58
- fetchedAt: Date.now()
152
+ fetchedAt,
153
+ ...opts.forceRefresh && !kid ? { noKidRefetchedAt: fetchedAt } : {}
59
154
  });
60
- return jwks;
155
+ return {
156
+ jwks,
157
+ fromCache: false,
158
+ kid
159
+ };
61
160
  }
62
- return cached.jwks;
161
+ return {
162
+ jwks: cachedJwks,
163
+ fromCache: true,
164
+ kid,
165
+ noKidRefetchedAt: cached?.noKidRefetchedAt
166
+ };
63
167
  }
64
- /**
65
- * Performs local verification of an access token for your API.
66
- *
67
- * Can also be configured for remote verification.
68
- */
69
- async function verifyAccessToken(token, opts) {
168
+ async function verifyAccessTokenPayload(token, opts) {
70
169
  let payload;
71
170
  if (opts.jwksUrl && !opts?.remoteVerify?.force) try {
72
171
  payload = await verifyJwsAccessToken(token, {
@@ -114,5 +213,58 @@ async function verifyAccessToken(token, opts) {
114
213
  }
115
214
  return payload;
116
215
  }
216
+ function throwDpopUnauthorized(message, error) {
217
+ throw new APIError("UNAUTHORIZED", error ? {
218
+ message,
219
+ error,
220
+ error_description: message
221
+ } : { message });
222
+ }
223
+ /**
224
+ * Performs local verification of a bearer access token for your API.
225
+ *
226
+ * Can also be configured for remote verification. DPoP-bound access tokens
227
+ * require {@link verifyAccessTokenRequest}, because sender-constraining cannot
228
+ * be verified without the HTTP method, URL, Authorization scheme, DPoP proof,
229
+ * and access-token hash. This function rejects DPoP-bound tokens; reach for it
230
+ * only when you hold a raw token string and intentionally accept bearer tokens
231
+ * alone.
232
+ */
233
+ async function verifyBearerToken(token, opts) {
234
+ const payload = await verifyAccessTokenPayload(token, opts);
235
+ if (getDpopJktFromPayload(payload)) throwDpopUnauthorized("DPoP-bound access token requires verifyAccessTokenRequest", "invalid_token");
236
+ return payload;
237
+ }
238
+ /**
239
+ * Verifies an HTTP resource request carrying an OAuth access token. This is the
240
+ * recommended resource-server entry point: it handles both bearer and
241
+ * DPoP-bound tokens, the bearer case being the request with no DPoP proof.
242
+ *
243
+ * It performs the same token validation as {@link verifyBearerToken}, then adds
244
+ * the RFC 9449 sender-constraint checks that need request context: authorization
245
+ * scheme, method, URL, DPoP proof, `ath`, and `cnf.jkt` binding.
246
+ */
247
+ async function verifyAccessTokenRequest(request, opts) {
248
+ const authorization = parseAccessTokenAuthorization(request.authorizationHeader);
249
+ if (!authorization?.token) throwDpopUnauthorized("missing authorization header");
250
+ if (authorization.scheme === "Unknown") throwDpopUnauthorized("authorization scheme must be Bearer or DPoP", "invalid_token");
251
+ const payload = await verifyAccessTokenPayload(authorization.token, opts);
252
+ try {
253
+ await enforceDpopBinding({
254
+ payload,
255
+ authorization,
256
+ proofJwt: request.dpopProofJwt,
257
+ method: request.method,
258
+ url: request.url,
259
+ replayStore: opts.dpop?.replayStore ?? defaultDpopReplayStore,
260
+ proofMaxAgeSeconds: opts.dpop?.proofMaxAgeSeconds,
261
+ signingAlgorithms: opts.dpop?.signingAlgorithms
262
+ });
263
+ } catch (error) {
264
+ if (isDpopBindingError(error)) throwDpopUnauthorized(error.message, error.code);
265
+ throw error;
266
+ }
267
+ return payload;
268
+ }
117
269
  //#endregion
118
- export { getJwks, verifyAccessToken, verifyJwsAccessToken };
270
+ export { getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken };
@@ -473,6 +473,7 @@ declare const socialProviders: {
473
473
  audience: string | string[];
474
474
  maxTokenAge: string;
475
475
  issuer: string | undefined;
476
+ verifyClaims: (claims: Record<string, unknown>) => boolean;
476
477
  };
477
478
  getUserInfo(token: OAuth2Tokens & {
478
479
  user?: {
@@ -172,6 +172,16 @@ declare const microsoft: (options: MicrosoftOptions) => {
172
172
  * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
173
173
  */
174
174
  issuer: string | undefined;
175
+ /**
176
+ * The multi-tenant endpoints (common/organizations/consumers) skip the
177
+ * issuer check above because the issuer varies per tenant, and the
178
+ * organizations and consumers JWKS sets overlap. Enforce the tenant
179
+ * binding explicitly so a token from a disallowed account class cannot
180
+ * pass: the issuer must name the token's own tenant, and the account
181
+ * class must match the configured restriction.
182
+ * @see https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
183
+ */
184
+ verifyClaims: (claims: Record<string, unknown>) => boolean;
175
185
  };
176
186
  getUserInfo(token: OAuth2Tokens & {
177
187
  user?: {