@better-auth/core 1.7.0-beta.5 → 1.7.0-beta.7
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.
- package/dist/api/index.d.mts +44 -1
- package/dist/api/index.mjs +40 -1
- package/dist/context/global.mjs +1 -1
- package/dist/context/transaction.d.mts +7 -4
- package/dist/context/transaction.mjs +6 -3
- package/dist/db/adapter/factory.mjs +57 -31
- package/dist/db/adapter/index.d.mts +54 -10
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/type.d.mts +12 -7
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/create-authorization-url.d.mts +3 -1
- package/dist/oauth2/create-authorization-url.mjs +3 -1
- package/dist/oauth2/dpop.d.mts +142 -0
- package/dist/oauth2/dpop.mjs +246 -0
- package/dist/oauth2/index.d.mts +4 -3
- package/dist/oauth2/index.mjs +3 -2
- package/dist/oauth2/oauth-provider.d.mts +37 -3
- package/dist/oauth2/refresh-access-token.mjs +15 -1
- package/dist/oauth2/verify.d.mts +74 -15
- package/dist/oauth2/verify.mjs +172 -20
- package/dist/social-providers/apple.d.mts +2 -0
- package/dist/social-providers/atlassian.d.mts +2 -0
- package/dist/social-providers/cognito.d.mts +2 -0
- package/dist/social-providers/discord.d.mts +2 -0
- package/dist/social-providers/dropbox.d.mts +2 -0
- package/dist/social-providers/facebook.d.mts +2 -0
- package/dist/social-providers/figma.d.mts +2 -0
- package/dist/social-providers/github.d.mts +2 -0
- package/dist/social-providers/gitlab.d.mts +2 -0
- package/dist/social-providers/google.d.mts +2 -0
- package/dist/social-providers/huggingface.d.mts +2 -0
- package/dist/social-providers/index.d.mts +71 -0
- package/dist/social-providers/kakao.d.mts +2 -0
- package/dist/social-providers/kick.d.mts +2 -0
- package/dist/social-providers/line.d.mts +2 -0
- package/dist/social-providers/linear.d.mts +2 -0
- package/dist/social-providers/linkedin.d.mts +2 -0
- package/dist/social-providers/microsoft-entra-id.d.mts +12 -0
- package/dist/social-providers/microsoft-entra-id.mjs +17 -2
- package/dist/social-providers/naver.d.mts +2 -0
- package/dist/social-providers/notion.d.mts +2 -0
- package/dist/social-providers/paybin.d.mts +2 -0
- package/dist/social-providers/paypal.d.mts +2 -0
- package/dist/social-providers/polar.d.mts +2 -0
- package/dist/social-providers/railway.d.mts +2 -0
- package/dist/social-providers/reddit.d.mts +2 -0
- package/dist/social-providers/reddit.mjs +1 -1
- package/dist/social-providers/roblox.d.mts +2 -0
- package/dist/social-providers/salesforce.d.mts +2 -0
- package/dist/social-providers/slack.d.mts +2 -0
- package/dist/social-providers/spotify.d.mts +2 -0
- package/dist/social-providers/tiktok.d.mts +2 -0
- package/dist/social-providers/twitch.d.mts +2 -0
- package/dist/social-providers/twitter.d.mts +2 -0
- package/dist/social-providers/vercel.d.mts +2 -0
- package/dist/social-providers/vk.d.mts +2 -0
- package/dist/social-providers/wechat.d.mts +2 -0
- package/dist/social-providers/wechat.mjs +1 -1
- package/dist/social-providers/zoom.d.mts +2 -0
- package/dist/types/context.d.mts +17 -0
- package/dist/types/init-options.d.mts +45 -5
- package/dist/types/plugin-client.d.mts +12 -2
- package/dist/utils/host.d.mts +1 -1
- package/dist/utils/host.mjs +7 -0
- package/dist/utils/url.mjs +4 -3
- package/package.json +5 -5
- package/src/api/index.ts +82 -0
- package/src/context/transaction.ts +45 -12
- package/src/db/adapter/factory.ts +127 -72
- package/src/db/adapter/index.ts +54 -9
- package/src/db/adapter/types.ts +1 -0
- package/src/db/type.ts +12 -7
- package/src/oauth2/create-authorization-url.ts +4 -0
- package/src/oauth2/dpop.ts +568 -0
- package/src/oauth2/index.ts +45 -1
- package/src/oauth2/oauth-provider.ts +40 -2
- package/src/oauth2/refresh-access-token.ts +27 -3
- package/src/oauth2/verify-id-token.ts +2 -0
- package/src/oauth2/verify.ts +329 -66
- package/src/social-providers/microsoft-entra-id.ts +44 -1
- package/src/social-providers/reddit.ts +5 -1
- package/src/social-providers/wechat.ts +8 -1
- package/src/types/context.ts +18 -0
- package/src/types/init-options.ts +40 -8
- package/src/types/plugin-client.ts +16 -2
- package/src/utils/host.ts +25 -1
- package/src/utils/url.ts +10 -4
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { JWK, JWTPayload } from "jose";
|
|
2
|
+
|
|
3
|
+
//#region src/oauth2/dpop.d.ts
|
|
4
|
+
declare const DPOP_AUTHORIZATION_SCHEME = "DPoP";
|
|
5
|
+
declare const BEARER_AUTHORIZATION_SCHEME = "Bearer";
|
|
6
|
+
declare const DPOP_PROOF_TYPE = "dpop+jwt";
|
|
7
|
+
declare const DPOP_SIGNING_ALGORITHMS: readonly ["EdDSA", "ES256", "ES512", "PS256", "RS256"];
|
|
8
|
+
type DpopSigningAlgorithm = (typeof DPOP_SIGNING_ALGORITHMS)[number];
|
|
9
|
+
type AccessTokenAuthorizationScheme = "Bearer" | "DPoP" | "Unknown";
|
|
10
|
+
interface AccessTokenAuthorization {
|
|
11
|
+
scheme: AccessTokenAuthorizationScheme;
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
14
|
+
type DpopProofErrorCode = "invalid_dpop_proof";
|
|
15
|
+
type DpopProofError = Error & {
|
|
16
|
+
code: DpopProofErrorCode;
|
|
17
|
+
};
|
|
18
|
+
interface DpopReplayReservation {
|
|
19
|
+
key: string;
|
|
20
|
+
expiresAt: Date;
|
|
21
|
+
now: Date;
|
|
22
|
+
}
|
|
23
|
+
interface DpopReplayStore {
|
|
24
|
+
reserve: (reservation: DpopReplayReservation) => Promise<boolean> | boolean;
|
|
25
|
+
}
|
|
26
|
+
declare function createInMemoryDpopReplayStore(): DpopReplayStore;
|
|
27
|
+
/**
|
|
28
|
+
* The single-use reservation capability a {@link createDpopReplayStore} needs:
|
|
29
|
+
* the auth context's `internalAdapter.reserveVerificationValue`. Kept structural
|
|
30
|
+
* so core does not depend on the adapter implementation.
|
|
31
|
+
*/
|
|
32
|
+
interface DpopReplayReservations {
|
|
33
|
+
reserveVerificationValue: (data: {
|
|
34
|
+
identifier: string;
|
|
35
|
+
value: string;
|
|
36
|
+
expiresAt: Date;
|
|
37
|
+
}) => Promise<boolean>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Database-backed DPoP proof replay store built on the auth context's
|
|
41
|
+
* verification reservation primitive (`internalAdapter.reserveVerificationValue`),
|
|
42
|
+
* the same atomic single-use mechanism that guards SAML assertion ids and other
|
|
43
|
+
* one-time tokens. A replayed proof collides on the deterministic reservation id
|
|
44
|
+
* so `reserve` returns `false`, giving cross-instance anti-replay. Prefer this
|
|
45
|
+
* over {@link createInMemoryDpopReplayStore} for any multi-instance or serverless
|
|
46
|
+
* resource server. Requires database-backed verification storage; a
|
|
47
|
+
* secondary-storage-only deployment rejects the proof (fails closed).
|
|
48
|
+
*/
|
|
49
|
+
declare function createDpopReplayStore(reservations: DpopReplayReservations): DpopReplayStore;
|
|
50
|
+
interface VerifyDpopProofOptions {
|
|
51
|
+
proofJwt: string;
|
|
52
|
+
method: string;
|
|
53
|
+
url: string;
|
|
54
|
+
accessToken?: string;
|
|
55
|
+
expectedJkt?: string;
|
|
56
|
+
requireAth?: boolean;
|
|
57
|
+
nowSeconds?: number;
|
|
58
|
+
proofMaxAgeSeconds?: number;
|
|
59
|
+
signingAlgorithms?: readonly string[];
|
|
60
|
+
replayStore?: DpopReplayStore;
|
|
61
|
+
}
|
|
62
|
+
interface VerifiedDpopProof {
|
|
63
|
+
jwk: JWK;
|
|
64
|
+
jkt: string;
|
|
65
|
+
jti: string;
|
|
66
|
+
htm: string;
|
|
67
|
+
htu: string;
|
|
68
|
+
iat: number;
|
|
69
|
+
ath?: string;
|
|
70
|
+
replayKey: string;
|
|
71
|
+
expiresAt: Date;
|
|
72
|
+
}
|
|
73
|
+
declare function createDpopProofError(code: DpopProofErrorCode, message: string): DpopProofError;
|
|
74
|
+
declare function isDpopProofError(error: unknown): error is DpopProofError;
|
|
75
|
+
declare function parseAccessTokenAuthorization(authorization: string | null | undefined): AccessTokenAuthorization | undefined;
|
|
76
|
+
declare function stripAccessTokenAuthorizationScheme(token: string): string;
|
|
77
|
+
declare function normalizeDpopHtu(url: string): string;
|
|
78
|
+
declare function deriveDpopAth(accessToken: string): Promise<string>;
|
|
79
|
+
declare function deriveDpopJkt(jwk: JWK): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Extracts the DPoP key thumbprint from an RFC 7800 `cnf` confirmation. The
|
|
82
|
+
* input is untrusted (a JWT claim, a JSON column), so any shape other than an
|
|
83
|
+
* object carrying a non-empty string `jkt` (a primitive, an array, a different
|
|
84
|
+
* confirmation method such as mTLS `x5t#S256`) yields `undefined` instead of
|
|
85
|
+
* throwing.
|
|
86
|
+
*/
|
|
87
|
+
declare function getConfirmationJkt(confirmation: unknown): string | undefined;
|
|
88
|
+
declare function getDpopJktFromPayload(payload: JWTPayload): string | undefined;
|
|
89
|
+
declare function verifyDpopProof({
|
|
90
|
+
proofJwt,
|
|
91
|
+
method,
|
|
92
|
+
url,
|
|
93
|
+
accessToken,
|
|
94
|
+
expectedJkt,
|
|
95
|
+
requireAth,
|
|
96
|
+
nowSeconds,
|
|
97
|
+
proofMaxAgeSeconds,
|
|
98
|
+
signingAlgorithms,
|
|
99
|
+
replayStore
|
|
100
|
+
}: VerifyDpopProofOptions): Promise<VerifiedDpopProof>;
|
|
101
|
+
type DpopBindingErrorCode = "invalid_token" | "invalid_dpop_proof";
|
|
102
|
+
type DpopBindingError = Error & {
|
|
103
|
+
code: DpopBindingErrorCode;
|
|
104
|
+
};
|
|
105
|
+
declare function createDpopBindingError(code: DpopBindingErrorCode, message: string): DpopBindingError;
|
|
106
|
+
declare function isDpopBindingError(error: unknown): error is DpopBindingError;
|
|
107
|
+
interface EnforceDpopBindingParams {
|
|
108
|
+
/** The already-verified access-token payload (from JWKS or introspection). */
|
|
109
|
+
payload: JWTPayload;
|
|
110
|
+
/** The parsed `Authorization` header (scheme + token). */
|
|
111
|
+
authorization: AccessTokenAuthorization;
|
|
112
|
+
/** The `DPoP` proof header value, if any. */
|
|
113
|
+
proofJwt: string | null | undefined;
|
|
114
|
+
method: string;
|
|
115
|
+
url: string;
|
|
116
|
+
replayStore?: DpopReplayStore;
|
|
117
|
+
proofMaxAgeSeconds?: number;
|
|
118
|
+
signingAlgorithms?: readonly string[];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Enforces the RFC 9449 §7.1 sender-constraint check for a resource request,
|
|
122
|
+
* given an access-token payload that has already been validated (by JWKS or
|
|
123
|
+
* introspection). This is the single source of truth for the
|
|
124
|
+
* "is the token DPoP-bound? then require the DPoP scheme, a proof, and a
|
|
125
|
+
* matching key" decision, shared by every resource-server entry point.
|
|
126
|
+
*
|
|
127
|
+
* Throws a {@link DpopBindingError} on any mismatch so callers map the
|
|
128
|
+
* `invalid_token` / `invalid_dpop_proof` code into their own transport. Returns
|
|
129
|
+
* normally for a valid bearer token (no `cnf.jkt`, no DPoP scheme).
|
|
130
|
+
*/
|
|
131
|
+
declare function enforceDpopBinding({
|
|
132
|
+
payload,
|
|
133
|
+
authorization,
|
|
134
|
+
proofJwt,
|
|
135
|
+
method,
|
|
136
|
+
url,
|
|
137
|
+
replayStore,
|
|
138
|
+
proofMaxAgeSeconds,
|
|
139
|
+
signingAlgorithms
|
|
140
|
+
}: EnforceDpopBindingParams): Promise<void>;
|
|
141
|
+
//#endregion
|
|
142
|
+
export { 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 };
|
|
@@ -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 };
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { additionalAuthorizationParamsSchema } from "./authorization-params.mjs";
|
|
2
2
|
import { decodeBasicCredentials, encodeBasicCredentials } from "./basic-credentials.mjs";
|
|
3
3
|
import { CLIENT_ASSERTION_TYPE, ClientAssertionContext, ClientAssertionGetter, ClientAssertionGrantType, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, PrivateKeyJwtClientAssertionGetterOptions, PrivateKeyJwtSigningAlgorithm, createPrivateKeyJwtClientAssertionGetter, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion } from "./client-assertion.mjs";
|
|
4
|
-
import { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, OAuthIdTokenConfig, ProviderGrantAuthority, ProviderOptions, UpstreamProvider } from "./oauth-provider.mjs";
|
|
4
|
+
import { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, OAuthIdTokenConfig, OAuthRefreshContext, ProviderGrantAuthority, ProviderOptions, UpstreamProvider } from "./oauth-provider.mjs";
|
|
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,
|
|
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,
|
|
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, type OAuthRefreshContext, 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 };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -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,
|
|
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,
|
|
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 };
|
|
@@ -69,6 +69,17 @@ type OAuth2UserInfo = {
|
|
|
69
69
|
image?: string | undefined;
|
|
70
70
|
emailVerified: boolean;
|
|
71
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Request metadata available to provider refresh hooks.
|
|
74
|
+
*
|
|
75
|
+
* The refresh flow may be triggered by endpoints such as `getAccessToken` or
|
|
76
|
+
* `refreshToken`; this context gives provider hooks access to the triggering
|
|
77
|
+
* request without exposing the full endpoint implementation surface.
|
|
78
|
+
*/
|
|
79
|
+
interface OAuthRefreshContext {
|
|
80
|
+
headers?: Headers | undefined;
|
|
81
|
+
request?: Request | undefined;
|
|
82
|
+
}
|
|
72
83
|
/**
|
|
73
84
|
* The result of building a provider authorization URL.
|
|
74
85
|
*
|
|
@@ -128,6 +139,12 @@ interface UpstreamProvider<T extends Record<string, any> = Record<string, any>,
|
|
|
128
139
|
redirectURI: string;
|
|
129
140
|
display?: string | undefined;
|
|
130
141
|
loginHint?: string | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* OIDC nonce generated by the redirect initiator and persisted in OAuth
|
|
144
|
+
* state. Providers that set `requiresIdTokenNonce` must forward this to
|
|
145
|
+
* the authorization URL as the `nonce` parameter.
|
|
146
|
+
*/
|
|
147
|
+
idTokenNonce?: string | undefined;
|
|
131
148
|
/**
|
|
132
149
|
* Extra query parameters to append to the authorization URL.
|
|
133
150
|
* Providers forward these to the shared `createAuthorizationURL` helper,
|
|
@@ -144,6 +161,12 @@ interface UpstreamProvider<T extends Record<string, any> = Record<string, any>,
|
|
|
144
161
|
deviceId?: string | undefined;
|
|
145
162
|
}) => Promise<OAuth2Tokens | null>;
|
|
146
163
|
getUserInfo: (token: OAuth2Tokens & {
|
|
164
|
+
/**
|
|
165
|
+
* OIDC nonce recovered from OAuth state. Providers that required an
|
|
166
|
+
* ID-token nonce must pass this to `verifyProviderIdToken` before
|
|
167
|
+
* trusting ID-token claims.
|
|
168
|
+
*/
|
|
169
|
+
expectedIdTokenNonce?: string | undefined;
|
|
147
170
|
/**
|
|
148
171
|
* The user object from the provider
|
|
149
172
|
* This is only available for some providers like Apple
|
|
@@ -160,9 +183,13 @@ interface UpstreamProvider<T extends Record<string, any> = Record<string, any>,
|
|
|
160
183
|
data: T;
|
|
161
184
|
} | null>;
|
|
162
185
|
/**
|
|
163
|
-
* Custom function to refresh a token
|
|
186
|
+
* Custom function to refresh a token.
|
|
187
|
+
*
|
|
188
|
+
* Receives request metadata from the endpoint that triggered the refresh.
|
|
189
|
+
* Providers that don't need request-scoped data can ignore the second
|
|
190
|
+
* argument.
|
|
164
191
|
*/
|
|
165
|
-
refreshAccessToken?: ((refreshToken: string) => Promise<OAuth2Tokens>) | undefined;
|
|
192
|
+
refreshAccessToken?: ((refreshToken: string, ctx?: OAuthRefreshContext) => Promise<OAuth2Tokens>) | undefined;
|
|
166
193
|
/**
|
|
167
194
|
* Declarative id_token verification config consumed by the shared
|
|
168
195
|
* `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
|
|
@@ -175,6 +202,13 @@ interface UpstreamProvider<T extends Record<string, any> = Record<string, any>,
|
|
|
175
202
|
* against this value to prevent authorization server mix-up attacks.
|
|
176
203
|
*/
|
|
177
204
|
issuer?: string | undefined;
|
|
205
|
+
/**
|
|
206
|
+
* Require shared OAuth redirect routes to bind ID-token verification to an
|
|
207
|
+
* authorization request nonce. When true, routes generate `idTokenNonce`,
|
|
208
|
+
* pass it to `createAuthorizationURL`, persist it in state, and provide it
|
|
209
|
+
* back to `getUserInfo` as `expectedIdTokenNonce`.
|
|
210
|
+
*/
|
|
211
|
+
requiresIdTokenNonce?: boolean | undefined;
|
|
178
212
|
/**
|
|
179
213
|
* Disable implicit sign up for new users. When set to true for the provider,
|
|
180
214
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
@@ -333,4 +367,4 @@ type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
333
367
|
requireEmailVerification?: boolean | undefined;
|
|
334
368
|
};
|
|
335
369
|
//#endregion
|
|
336
|
-
export { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, OAuthIdTokenConfig, ProviderGrantAuthority, ProviderOptions, UpstreamProvider };
|
|
370
|
+
export { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, OAuthIdTokenConfig, OAuthRefreshContext, ProviderGrantAuthority, ProviderOptions, UpstreamProvider };
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { applyTokenEndpointAuth } from "./token-endpoint-auth.mjs";
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
3
|
//#region src/oauth2/refresh-access-token.ts
|
|
4
|
+
const BLOCKED_REFRESH_TOKEN_PARAMS_SET = new Set([
|
|
5
|
+
"grant_type",
|
|
6
|
+
"refresh_token",
|
|
7
|
+
"__proto__",
|
|
8
|
+
"constructor",
|
|
9
|
+
"prototype"
|
|
10
|
+
]);
|
|
4
11
|
async function refreshAccessTokenRequest({ refreshToken, options, authentication, tokenEndpointAuth, tokenEndpoint, extraParams, resource }) {
|
|
5
12
|
options = typeof options === "function" ? await options() : options;
|
|
6
13
|
const request = buildRefreshAccessTokenRequest({
|
|
@@ -20,6 +27,13 @@ async function refreshAccessTokenRequest({ refreshToken, options, authentication
|
|
|
20
27
|
});
|
|
21
28
|
return request;
|
|
22
29
|
}
|
|
30
|
+
function applyRefreshExtraParams(body, extraParams) {
|
|
31
|
+
if (!extraParams) return;
|
|
32
|
+
for (const [key, value] of Object.entries(extraParams)) {
|
|
33
|
+
if (BLOCKED_REFRESH_TOKEN_PARAMS_SET.has(key)) continue;
|
|
34
|
+
body.set(key, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
function buildRefreshAccessTokenRequest({ refreshToken, options, extraParams, resource }) {
|
|
24
38
|
const body = new URLSearchParams();
|
|
25
39
|
const headers = {
|
|
@@ -30,7 +44,7 @@ function buildRefreshAccessTokenRequest({ refreshToken, options, extraParams, re
|
|
|
30
44
|
body.set("refresh_token", refreshToken);
|
|
31
45
|
if (resource) if (typeof resource === "string") body.append("resource", resource);
|
|
32
46
|
else for (const _resource of resource) body.append("resource", _resource);
|
|
33
|
-
if (extraParams)
|
|
47
|
+
if (extraParams) applyRefreshExtraParams(body, extraParams);
|
|
34
48
|
return {
|
|
35
49
|
body,
|
|
36
50
|
headers
|
package/dist/oauth2/verify.d.mts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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,
|
|
115
|
+
export { ResourceRequestInput, VerifyAccessTokenOptions, VerifyAccessTokenRequestOptions, getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken };
|