@better-auth/core 1.7.0-beta.4 → 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.
- package/dist/api/index.d.mts +47 -4
- 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/get-tables.mjs +3 -3
- package/dist/db/schema/account.d.mts +1 -1
- package/dist/db/schema/account.mjs +1 -1
- package/dist/db/type.d.mts +12 -7
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +5 -0
- package/dist/error/codes.mjs +5 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/create-authorization-url.d.mts +4 -1
- package/dist/oauth2/create-authorization-url.mjs +5 -2
- package/dist/oauth2/dpop.d.mts +142 -0
- package/dist/oauth2/dpop.mjs +246 -0
- package/dist/oauth2/index.d.mts +6 -3
- package/dist/oauth2/index.mjs +5 -2
- package/dist/oauth2/oauth-provider.d.mts +128 -9
- package/dist/oauth2/refresh-access-token.mjs +1 -1
- package/dist/oauth2/scopes.d.mts +76 -0
- package/dist/oauth2/scopes.mjs +96 -0
- package/dist/oauth2/utils.mjs +2 -1
- package/dist/oauth2/verify-id-token.d.mts +26 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +88 -15
- package/dist/oauth2/verify.mjs +187 -19
- package/dist/social-providers/apple.d.mts +14 -2
- package/dist/social-providers/apple.mjs +12 -36
- package/dist/social-providers/atlassian.d.mts +5 -1
- package/dist/social-providers/atlassian.mjs +4 -4
- package/dist/social-providers/cognito.d.mts +13 -2
- package/dist/social-providers/cognito.mjs +24 -32
- package/dist/social-providers/discord.d.mts +5 -1
- package/dist/social-providers/discord.mjs +7 -6
- package/dist/social-providers/dropbox.d.mts +5 -1
- package/dist/social-providers/dropbox.mjs +5 -5
- package/dist/social-providers/facebook.d.mts +21 -2
- package/dist/social-providers/facebook.mjs +46 -22
- package/dist/social-providers/figma.d.mts +5 -1
- package/dist/social-providers/figma.mjs +5 -5
- package/dist/social-providers/github.d.mts +5 -1
- package/dist/social-providers/github.mjs +4 -4
- package/dist/social-providers/gitlab.d.mts +5 -1
- package/dist/social-providers/gitlab.mjs +6 -6
- package/dist/social-providers/google.d.mts +29 -3
- package/dist/social-providers/google.mjs +24 -30
- package/dist/social-providers/huggingface.d.mts +5 -1
- package/dist/social-providers/huggingface.mjs +8 -8
- package/dist/social-providers/index.d.mts +222 -42
- package/dist/social-providers/kakao.d.mts +5 -1
- package/dist/social-providers/kakao.mjs +8 -8
- package/dist/social-providers/kick.d.mts +5 -1
- package/dist/social-providers/kick.mjs +4 -4
- package/dist/social-providers/line.d.mts +8 -2
- package/dist/social-providers/line.mjs +12 -14
- package/dist/social-providers/linear.d.mts +5 -1
- package/dist/social-providers/linear.mjs +4 -4
- package/dist/social-providers/linkedin.d.mts +5 -1
- package/dist/social-providers/linkedin.mjs +10 -10
- package/dist/social-providers/microsoft-entra-id.d.mts +41 -6
- package/dist/social-providers/microsoft-entra-id.mjs +40 -36
- package/dist/social-providers/naver.d.mts +5 -1
- package/dist/social-providers/naver.mjs +4 -4
- package/dist/social-providers/notion.d.mts +5 -1
- package/dist/social-providers/notion.mjs +4 -4
- package/dist/social-providers/paybin.d.mts +5 -1
- package/dist/social-providers/paybin.mjs +10 -10
- package/dist/social-providers/paypal.d.mts +5 -2
- package/dist/social-providers/paypal.mjs +8 -13
- package/dist/social-providers/polar.d.mts +5 -1
- package/dist/social-providers/polar.mjs +8 -8
- package/dist/social-providers/railway.d.mts +5 -1
- package/dist/social-providers/railway.mjs +9 -9
- package/dist/social-providers/reddit.d.mts +5 -1
- package/dist/social-providers/reddit.mjs +9 -8
- package/dist/social-providers/roblox.d.mts +5 -1
- package/dist/social-providers/roblox.mjs +5 -5
- package/dist/social-providers/salesforce.d.mts +5 -1
- package/dist/social-providers/salesforce.mjs +8 -8
- package/dist/social-providers/slack.d.mts +5 -1
- package/dist/social-providers/slack.mjs +9 -9
- package/dist/social-providers/spotify.d.mts +5 -1
- package/dist/social-providers/spotify.mjs +5 -5
- package/dist/social-providers/tiktok.d.mts +5 -1
- package/dist/social-providers/tiktok.mjs +9 -5
- package/dist/social-providers/twitch.d.mts +5 -1
- package/dist/social-providers/twitch.mjs +4 -4
- package/dist/social-providers/twitter.d.mts +6 -4
- package/dist/social-providers/twitter.mjs +9 -9
- package/dist/social-providers/vercel.d.mts +5 -1
- package/dist/social-providers/vercel.mjs +4 -7
- package/dist/social-providers/vk.d.mts +5 -1
- package/dist/social-providers/vk.mjs +5 -5
- package/dist/social-providers/wechat.d.mts +5 -1
- package/dist/social-providers/wechat.mjs +10 -6
- package/dist/social-providers/zoom.d.mts +6 -1
- package/dist/social-providers/zoom.mjs +15 -9
- package/dist/types/context.d.mts +27 -8
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +137 -6
- package/dist/types/plugin-client.d.mts +12 -2
- package/dist/utils/host.mjs +4 -0
- package/dist/utils/url.mjs +4 -3
- package/package.json +7 -7
- package/src/api/index.ts +82 -0
- package/src/context/transaction.ts +45 -12
- package/src/db/adapter/factory.ts +127 -64
- package/src/db/adapter/index.ts +54 -9
- package/src/db/adapter/types.ts +1 -0
- package/src/db/get-tables.ts +8 -3
- package/src/db/schema/account.ts +14 -2
- package/src/db/type.ts +12 -7
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +5 -0
- package/src/oauth2/create-authorization-url.ts +2 -2
- package/src/oauth2/dpop.ts +568 -0
- package/src/oauth2/index.ts +61 -2
- package/src/oauth2/oauth-provider.ts +140 -10
- package/src/oauth2/refresh-access-token.ts +2 -2
- package/src/oauth2/scopes.ts +118 -0
- package/src/oauth2/utils.ts +2 -5
- package/src/oauth2/verify-id-token.ts +111 -0
- package/src/oauth2/verify.ts +372 -58
- package/src/social-providers/apple.ts +24 -61
- package/src/social-providers/atlassian.ts +12 -8
- package/src/social-providers/cognito.ts +25 -47
- package/src/social-providers/discord.ts +19 -8
- package/src/social-providers/dropbox.ts +13 -7
- package/src/social-providers/facebook.ts +97 -51
- package/src/social-providers/figma.ts +13 -9
- package/src/social-providers/github.ts +12 -8
- package/src/social-providers/gitlab.ts +14 -8
- package/src/social-providers/google.ts +66 -47
- package/src/social-providers/huggingface.ts +12 -8
- package/src/social-providers/kakao.ts +16 -8
- package/src/social-providers/kick.ts +12 -7
- package/src/social-providers/line.ts +37 -37
- package/src/social-providers/linear.ts +12 -6
- package/src/social-providers/linkedin.ts +14 -10
- package/src/social-providers/microsoft-entra-id.ts +103 -59
- package/src/social-providers/naver.ts +12 -6
- package/src/social-providers/notion.ts +12 -6
- package/src/social-providers/paybin.ts +14 -11
- package/src/social-providers/paypal.ts +6 -25
- package/src/social-providers/polar.ts +12 -8
- package/src/social-providers/railway.ts +13 -9
- package/src/social-providers/reddit.ts +25 -10
- package/src/social-providers/roblox.ts +18 -7
- package/src/social-providers/salesforce.ts +12 -8
- package/src/social-providers/slack.ts +18 -9
- package/src/social-providers/spotify.ts +13 -7
- package/src/social-providers/tiktok.ts +13 -7
- package/src/social-providers/twitch.ts +12 -8
- package/src/social-providers/twitter.ts +17 -8
- package/src/social-providers/vercel.ts +16 -10
- package/src/social-providers/vk.ts +13 -7
- package/src/social-providers/wechat.ts +28 -9
- package/src/social-providers/zoom.ts +19 -6
- package/src/types/context.ts +26 -8
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +159 -8
- package/src/types/plugin-client.ts +16 -2
- package/src/utils/host.ts +15 -0
- 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,12 +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 { OAuth2Tokens, OAuth2UserInfo,
|
|
4
|
+
import { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, OAuthIdTokenConfig, 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";
|
|
10
|
+
import { includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, resolveRequestedScopes, unionGrantedScopes } from "./scopes.mjs";
|
|
9
11
|
import { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
|
|
10
12
|
import { authorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
|
|
11
|
-
import { getJwks,
|
|
12
|
-
|
|
13
|
+
import { ResourceRequestInput, VerifyAccessTokenOptions, VerifyAccessTokenRequestOptions, getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken } from "./verify.mjs";
|
|
14
|
+
import { supportsIdTokenSignIn, verifyProviderIdToken } from "./verify-id-token.mjs";
|
|
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 };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { includesGrantedScope, normalizeScopes, parseScopeField, readGrantedScopes, resolveRequestedScopes, unionGrantedScopes } from "./scopes.mjs";
|
|
1
2
|
import { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId } from "./utils.mjs";
|
|
2
3
|
import { RESERVED_AUTHORIZATION_PARAMS, RESERVED_AUTHORIZATION_PARAMS_SET, createAuthorizationURL } from "./create-authorization-url.mjs";
|
|
3
4
|
import { additionalAuthorizationParamsSchema } from "./authorization-params.mjs";
|
|
4
5
|
import { decodeBasicCredentials, encodeBasicCredentials } from "./basic-credentials.mjs";
|
|
5
6
|
import { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, createPrivateKeyJwtClientAssertionGetter, resolveClientAssertionParams, signPrivateKeyJwtClientAssertion } from "./client-assertion.mjs";
|
|
6
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";
|
|
7
9
|
import { refreshAccessToken, refreshAccessTokenRequest } from "./refresh-access-token.mjs";
|
|
8
10
|
import { authorizationCodeRequest, validateAuthorizationCode, validateToken } from "./validate-authorization-code.mjs";
|
|
9
|
-
import { getJwks,
|
|
10
|
-
|
|
11
|
+
import { getJwks, requestToResourceInput, verifyAccessTokenRequest, verifyBearerToken, verifyJwsAccessToken } from "./verify.mjs";
|
|
12
|
+
import { supportsIdTokenSignIn, verifyProviderIdToken } from "./verify-id-token.mjs";
|
|
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,5 +1,53 @@
|
|
|
1
1
|
import { Awaitable, LiteralString } from "../types/helper.mjs";
|
|
2
|
+
import { JWTVerifyGetKey } from "jose";
|
|
3
|
+
|
|
2
4
|
//#region src/oauth2/oauth-provider.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* id_token verification config for a social provider.
|
|
7
|
+
*
|
|
8
|
+
* Declares how a client-submitted id_token is verified. The shared verifier
|
|
9
|
+
* (`verifyProviderIdToken`) consumes this instead of each provider implementing its own
|
|
10
|
+
* boolean check, so verification is centralized and fail-closed: a provider without a config
|
|
11
|
+
* cannot accept a forged token by omission.
|
|
12
|
+
*/
|
|
13
|
+
type OAuthIdTokenConfig = {
|
|
14
|
+
/**
|
|
15
|
+
* JWKS resolver used to verify the JWS signature. Accepts a jose
|
|
16
|
+
* `createRemoteJWKSet` resolver or a key-resolving function
|
|
17
|
+
* `(protectedHeader) => key`.
|
|
18
|
+
*/
|
|
19
|
+
jwks: JWTVerifyGetKey; /** Expected `iss`. Omit for providers whose issuer varies per tenant. */
|
|
20
|
+
issuer?: (string | string[]) | undefined; /** Expected `aud`, usually the client ID. */
|
|
21
|
+
audience: string | string[]; /** Permitted JWS algorithms. Defaults to the token's `alg` header. */
|
|
22
|
+
algorithms?: string[] | undefined; /** Maximum token age passed to jose (e.g. `"1h"`). */
|
|
23
|
+
maxTokenAge?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* How the `nonce` claim is compared to the expected nonce.
|
|
26
|
+
* - `"exact"` (default): strict equality.
|
|
27
|
+
* - `"exact-or-sha256"`: matches the raw nonce or its SHA-256 hex digest (Apple).
|
|
28
|
+
*/
|
|
29
|
+
nonceComparison?: ("exact" | "exact-or-sha256") | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Accept non-JWS (opaque) tokens without signature verification. Identity is then
|
|
32
|
+
* resolved by getUserInfo from the access token via the provider userinfo endpoint,
|
|
33
|
+
* which validates it (e.g. Facebook Graph access tokens).
|
|
34
|
+
*/
|
|
35
|
+
allowOpaqueToken?: boolean | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Provider-specific claim check applied after the signature, issuer,
|
|
38
|
+
* audience, max-age, and nonce checks pass. Return `false` to reject the
|
|
39
|
+
* token. Used to enforce constraints the standard checks cannot express,
|
|
40
|
+
* e.g. Google's hosted-domain (`hd`) restriction. Omitted by providers
|
|
41
|
+
* that have no extra claim requirement.
|
|
42
|
+
*/
|
|
43
|
+
verifyClaims?: ((claims: Record<string, unknown>) => boolean) | undefined;
|
|
44
|
+
} | {
|
|
45
|
+
/**
|
|
46
|
+
* Custom verifier for providers that cannot verify against a local JWKS, such as a
|
|
47
|
+
* remote verification endpoint (e.g. LINE).
|
|
48
|
+
*/
|
|
49
|
+
verify: (token: string, nonce?: string) => Promise<boolean>;
|
|
50
|
+
};
|
|
3
51
|
interface OAuth2Tokens {
|
|
4
52
|
tokenType?: string | undefined;
|
|
5
53
|
accessToken?: string | undefined;
|
|
@@ -21,8 +69,58 @@ type OAuth2UserInfo = {
|
|
|
21
69
|
image?: string | undefined;
|
|
22
70
|
emailVerified: boolean;
|
|
23
71
|
};
|
|
24
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The result of building a provider authorization URL.
|
|
74
|
+
*
|
|
75
|
+
* `requestedScopes` is the effective set of scopes encoded in the URL (the
|
|
76
|
+
* provider's built-in defaults + configured `options.scope` + per-request
|
|
77
|
+
* `scopes`, composed by `resolveRequestedScopes`). Callers persist it so the
|
|
78
|
+
* callback can fall back to the request when the provider omits `scope` from
|
|
79
|
+
* its token response (RFC 6749 §5.1).
|
|
80
|
+
*/
|
|
81
|
+
interface AuthorizationURLResult {
|
|
82
|
+
url: URL;
|
|
83
|
+
requestedScopes: string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How much an RP trusts a provider's echoed token-response `scope` when
|
|
87
|
+
* persisting `account.grantedScopes`.
|
|
88
|
+
*
|
|
89
|
+
* - `"full-grant"`: the echo is the user's complete current grant, so the seam
|
|
90
|
+
* replaces the stored grant with it. This is the only path that may narrow
|
|
91
|
+
* the grant. Declare it only for providers whose token response reports the
|
|
92
|
+
* full combined grant, e.g. Google with `include_granted_scopes`.
|
|
93
|
+
* - `"projection"`: the echo is this request's subset, so the seam unions it
|
|
94
|
+
* onto the stored grant. The safe default for every provider.
|
|
95
|
+
* - `"absent-echo"`: the provider omitted `scope`, so the grant equals what was
|
|
96
|
+
* requested (RFC 6749 §5.1) and the seam unions the requested set. Resolved
|
|
97
|
+
* at runtime by the persistence seam, never declared by a provider.
|
|
98
|
+
*
|
|
99
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
|
|
100
|
+
*/
|
|
101
|
+
type GrantAuthority = "full-grant" | "projection" | "absent-echo";
|
|
102
|
+
/**
|
|
103
|
+
* The authority a provider may declare for its own echoed scope. `"absent-echo"`
|
|
104
|
+
* is excluded because it is a runtime condition (an omitted echo), not a
|
|
105
|
+
* provider trait.
|
|
106
|
+
*/
|
|
107
|
+
type ProviderGrantAuthority = Exclude<GrantAuthority, "absent-echo">;
|
|
108
|
+
interface UpstreamProvider<T extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Partial<ProviderOptions>> {
|
|
25
109
|
id: LiteralString;
|
|
110
|
+
/**
|
|
111
|
+
* The path the provider redirects back to, relative to the app base URL,
|
|
112
|
+
* e.g. `/callback/google`.
|
|
113
|
+
*/
|
|
114
|
+
callbackPath: string;
|
|
115
|
+
/**
|
|
116
|
+
* How the persistence seam treats this provider's echoed token-response
|
|
117
|
+
* `scope`. Declare `"full-grant"` only when the echo is the user's complete
|
|
118
|
+
* current grant (e.g. Google with `include_granted_scopes`); otherwise the
|
|
119
|
+
* echo is unioned onto the stored grant.
|
|
120
|
+
*
|
|
121
|
+
* @default "projection"
|
|
122
|
+
*/
|
|
123
|
+
grantAuthority?: ProviderGrantAuthority | undefined;
|
|
26
124
|
createAuthorizationURL: (data: {
|
|
27
125
|
state: string;
|
|
28
126
|
codeVerifier: string;
|
|
@@ -37,7 +135,7 @@ interface OAuthProvider<T extends Record<string, any> = Record<string, any>, O e
|
|
|
37
135
|
* before applying them.
|
|
38
136
|
*/
|
|
39
137
|
additionalParams?: Record<string, string> | undefined;
|
|
40
|
-
}) => Awaitable<
|
|
138
|
+
}) => Awaitable<AuthorizationURLResult>;
|
|
41
139
|
name: string;
|
|
42
140
|
validateAuthorizationCode: (data: {
|
|
43
141
|
code: string;
|
|
@@ -65,14 +163,12 @@ interface OAuthProvider<T extends Record<string, any> = Record<string, any>, O e
|
|
|
65
163
|
* Custom function to refresh a token
|
|
66
164
|
*/
|
|
67
165
|
refreshAccessToken?: ((refreshToken: string) => Promise<OAuth2Tokens>) | undefined;
|
|
68
|
-
revokeToken?: ((token: string) => Promise<void>) | undefined;
|
|
69
166
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @returns True if the id token is valid, false otherwise
|
|
167
|
+
* Declarative id_token verification config consumed by the shared
|
|
168
|
+
* `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
|
|
169
|
+
* verify method, which keeps verification centralized and fail-closed.
|
|
74
170
|
*/
|
|
75
|
-
|
|
171
|
+
idToken?: OAuthIdTokenConfig | undefined;
|
|
76
172
|
/**
|
|
77
173
|
* The expected issuer identifier for this provider (RFC 9207).
|
|
78
174
|
* When set, the callback handler validates the `iss` query parameter
|
|
@@ -212,6 +308,29 @@ type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
212
308
|
* @default false
|
|
213
309
|
*/
|
|
214
310
|
overrideUserInfoOnSignIn?: boolean | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* Require this provider's email to be verified before a session is created.
|
|
313
|
+
*
|
|
314
|
+
* When the provider reports the email as unverified, the user and account are
|
|
315
|
+
* still created/linked, but no session is issued: the OAuth callback redirects
|
|
316
|
+
* with `?error=email_not_verified` and id-token sign-in returns a `403`
|
|
317
|
+
* `EMAIL_NOT_VERIFIED`. A verification email is (re)sent per the
|
|
318
|
+
* `emailVerification` settings (`sendOnSignUp` / `sendOnSignIn`).
|
|
319
|
+
*
|
|
320
|
+
* The gate checks the local user's verification state, not the provider's
|
|
321
|
+
* claim on each request: a user already verified through another method (or a
|
|
322
|
+
* prior verified sign-in) keeps access even if the provider later reports the
|
|
323
|
+
* email as unverified.
|
|
324
|
+
*
|
|
325
|
+
* This is opt-in per provider and is independent of
|
|
326
|
+
* `emailAndPassword.requireEmailVerification`; enabling that does not gate
|
|
327
|
+
* social sign-in. Only enable it for providers that report a trustworthy
|
|
328
|
+
* `email_verified` signal: several providers always report the email as
|
|
329
|
+
* unverified, which would block every sign-in.
|
|
330
|
+
*
|
|
331
|
+
* @default false
|
|
332
|
+
*/
|
|
333
|
+
requireEmailVerification?: boolean | undefined;
|
|
215
334
|
};
|
|
216
335
|
//#endregion
|
|
217
|
-
export { OAuth2Tokens, OAuth2UserInfo,
|
|
336
|
+
export { AuthorizationURLResult, GrantAuthority, OAuth2Tokens, OAuth2UserInfo, OAuthIdTokenConfig, ProviderGrantAuthority, ProviderOptions, UpstreamProvider };
|
|
@@ -56,7 +56,7 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
|
|
|
56
56
|
accessToken: data.access_token,
|
|
57
57
|
refreshToken: data.refresh_token,
|
|
58
58
|
tokenType: data.token_type,
|
|
59
|
-
scopes: data.scope?.split(" "),
|
|
59
|
+
scopes: Array.isArray(data.scope) ? data.scope : data.scope?.split(" "),
|
|
60
60
|
idToken: data.id_token
|
|
61
61
|
};
|
|
62
62
|
if (data.expires_in) {
|