@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
|
@@ -1,5 +1,59 @@
|
|
|
1
|
+
import type { JWTVerifyGetKey } from "jose";
|
|
1
2
|
import type { Awaitable, LiteralString } from "../types";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* id_token verification config for a social provider.
|
|
6
|
+
*
|
|
7
|
+
* Declares how a client-submitted id_token is verified. The shared verifier
|
|
8
|
+
* (`verifyProviderIdToken`) consumes this instead of each provider implementing its own
|
|
9
|
+
* boolean check, so verification is centralized and fail-closed: a provider without a config
|
|
10
|
+
* cannot accept a forged token by omission.
|
|
11
|
+
*/
|
|
12
|
+
export type OAuthIdTokenConfig =
|
|
13
|
+
| {
|
|
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;
|
|
20
|
+
/** Expected `iss`. Omit for providers whose issuer varies per tenant. */
|
|
21
|
+
issuer?: (string | string[]) | undefined;
|
|
22
|
+
/** Expected `aud`, usually the client ID. */
|
|
23
|
+
audience: string | string[];
|
|
24
|
+
/** Permitted JWS algorithms. Defaults to the token's `alg` header. */
|
|
25
|
+
algorithms?: string[] | undefined;
|
|
26
|
+
/** Maximum token age passed to jose (e.g. `"1h"`). */
|
|
27
|
+
maxTokenAge?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* How the `nonce` claim is compared to the expected nonce.
|
|
30
|
+
* - `"exact"` (default): strict equality.
|
|
31
|
+
* - `"exact-or-sha256"`: matches the raw nonce or its SHA-256 hex digest (Apple).
|
|
32
|
+
*/
|
|
33
|
+
nonceComparison?: ("exact" | "exact-or-sha256") | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Accept non-JWS (opaque) tokens without signature verification. Identity is then
|
|
36
|
+
* resolved by getUserInfo from the access token via the provider userinfo endpoint,
|
|
37
|
+
* which validates it (e.g. Facebook Graph access tokens).
|
|
38
|
+
*/
|
|
39
|
+
allowOpaqueToken?: boolean | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Provider-specific claim check applied after the signature, issuer,
|
|
42
|
+
* audience, max-age, and nonce checks pass. Return `false` to reject the
|
|
43
|
+
* token. Used to enforce constraints the standard checks cannot express,
|
|
44
|
+
* e.g. Google's hosted-domain (`hd`) restriction. Omitted by providers
|
|
45
|
+
* that have no extra claim requirement.
|
|
46
|
+
*/
|
|
47
|
+
verifyClaims?: ((claims: Record<string, unknown>) => boolean) | undefined;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
/**
|
|
51
|
+
* Custom verifier for providers that cannot verify against a local JWKS, such as a
|
|
52
|
+
* remote verification endpoint (e.g. LINE).
|
|
53
|
+
*/
|
|
54
|
+
verify: (token: string, nonce?: string) => Promise<boolean>;
|
|
55
|
+
};
|
|
56
|
+
|
|
3
57
|
export interface OAuth2Tokens {
|
|
4
58
|
tokenType?: string | undefined;
|
|
5
59
|
accessToken?: string | undefined;
|
|
@@ -23,11 +77,64 @@ export type OAuth2UserInfo = {
|
|
|
23
77
|
emailVerified: boolean;
|
|
24
78
|
};
|
|
25
79
|
|
|
26
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The result of building a provider authorization URL.
|
|
82
|
+
*
|
|
83
|
+
* `requestedScopes` is the effective set of scopes encoded in the URL (the
|
|
84
|
+
* provider's built-in defaults + configured `options.scope` + per-request
|
|
85
|
+
* `scopes`, composed by `resolveRequestedScopes`). Callers persist it so the
|
|
86
|
+
* callback can fall back to the request when the provider omits `scope` from
|
|
87
|
+
* its token response (RFC 6749 §5.1).
|
|
88
|
+
*/
|
|
89
|
+
export interface AuthorizationURLResult {
|
|
90
|
+
url: URL;
|
|
91
|
+
requestedScopes: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* How much an RP trusts a provider's echoed token-response `scope` when
|
|
96
|
+
* persisting `account.grantedScopes`.
|
|
97
|
+
*
|
|
98
|
+
* - `"full-grant"`: the echo is the user's complete current grant, so the seam
|
|
99
|
+
* replaces the stored grant with it. This is the only path that may narrow
|
|
100
|
+
* the grant. Declare it only for providers whose token response reports the
|
|
101
|
+
* full combined grant, e.g. Google with `include_granted_scopes`.
|
|
102
|
+
* - `"projection"`: the echo is this request's subset, so the seam unions it
|
|
103
|
+
* onto the stored grant. The safe default for every provider.
|
|
104
|
+
* - `"absent-echo"`: the provider omitted `scope`, so the grant equals what was
|
|
105
|
+
* requested (RFC 6749 §5.1) and the seam unions the requested set. Resolved
|
|
106
|
+
* at runtime by the persistence seam, never declared by a provider.
|
|
107
|
+
*
|
|
108
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
|
|
109
|
+
*/
|
|
110
|
+
export type GrantAuthority = "full-grant" | "projection" | "absent-echo";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The authority a provider may declare for its own echoed scope. `"absent-echo"`
|
|
114
|
+
* is excluded because it is a runtime condition (an omitted echo), not a
|
|
115
|
+
* provider trait.
|
|
116
|
+
*/
|
|
117
|
+
export type ProviderGrantAuthority = Exclude<GrantAuthority, "absent-echo">;
|
|
118
|
+
|
|
119
|
+
export interface UpstreamProvider<
|
|
27
120
|
T extends Record<string, any> = Record<string, any>,
|
|
28
121
|
O extends Record<string, any> = Partial<ProviderOptions>,
|
|
29
122
|
> {
|
|
30
123
|
id: LiteralString;
|
|
124
|
+
/**
|
|
125
|
+
* The path the provider redirects back to, relative to the app base URL,
|
|
126
|
+
* e.g. `/callback/google`.
|
|
127
|
+
*/
|
|
128
|
+
callbackPath: string;
|
|
129
|
+
/**
|
|
130
|
+
* How the persistence seam treats this provider's echoed token-response
|
|
131
|
+
* `scope`. Declare `"full-grant"` only when the echo is the user's complete
|
|
132
|
+
* current grant (e.g. Google with `include_granted_scopes`); otherwise the
|
|
133
|
+
* echo is unioned onto the stored grant.
|
|
134
|
+
*
|
|
135
|
+
* @default "projection"
|
|
136
|
+
*/
|
|
137
|
+
grantAuthority?: ProviderGrantAuthority | undefined;
|
|
31
138
|
createAuthorizationURL: (data: {
|
|
32
139
|
state: string;
|
|
33
140
|
codeVerifier: string;
|
|
@@ -42,7 +149,7 @@ export interface OAuthProvider<
|
|
|
42
149
|
* before applying them.
|
|
43
150
|
*/
|
|
44
151
|
additionalParams?: Record<string, string> | undefined;
|
|
45
|
-
}) => Awaitable<
|
|
152
|
+
}) => Awaitable<AuthorizationURLResult>;
|
|
46
153
|
name: string;
|
|
47
154
|
validateAuthorizationCode: (data: {
|
|
48
155
|
code: string;
|
|
@@ -76,16 +183,12 @@ export interface OAuthProvider<
|
|
|
76
183
|
refreshAccessToken?:
|
|
77
184
|
| ((refreshToken: string) => Promise<OAuth2Tokens>)
|
|
78
185
|
| undefined;
|
|
79
|
-
revokeToken?: ((token: string) => Promise<void>) | undefined;
|
|
80
186
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @returns True if the id token is valid, false otherwise
|
|
187
|
+
* Declarative id_token verification config consumed by the shared
|
|
188
|
+
* `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
|
|
189
|
+
* verify method, which keeps verification centralized and fail-closed.
|
|
85
190
|
*/
|
|
86
|
-
|
|
87
|
-
| ((token: string, nonce?: string) => Promise<boolean>)
|
|
88
|
-
| undefined;
|
|
191
|
+
idToken?: OAuthIdTokenConfig | undefined;
|
|
89
192
|
/**
|
|
90
193
|
* The expected issuer identifier for this provider (RFC 9207).
|
|
91
194
|
* When set, the callback handler validates the `iss` query parameter
|
|
@@ -180,6 +283,10 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
180
283
|
emailVerified: boolean;
|
|
181
284
|
[key: string]: any;
|
|
182
285
|
};
|
|
286
|
+
// TODO: type as `Profile` once provider getUserInfo paths that return a
|
|
287
|
+
// narrower data shape than their declared profile are reconciled; today
|
|
288
|
+
// `any` is load-bearing for those (e.g. facebook) and tightening it ripples
|
|
289
|
+
// across ~10 providers, out of scope for the grant refactor.
|
|
183
290
|
data: any;
|
|
184
291
|
} | null>)
|
|
185
292
|
| undefined;
|
|
@@ -244,4 +351,27 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
244
351
|
* @default false
|
|
245
352
|
*/
|
|
246
353
|
overrideUserInfoOnSignIn?: boolean | undefined;
|
|
354
|
+
/**
|
|
355
|
+
* Require this provider's email to be verified before a session is created.
|
|
356
|
+
*
|
|
357
|
+
* When the provider reports the email as unverified, the user and account are
|
|
358
|
+
* still created/linked, but no session is issued: the OAuth callback redirects
|
|
359
|
+
* with `?error=email_not_verified` and id-token sign-in returns a `403`
|
|
360
|
+
* `EMAIL_NOT_VERIFIED`. A verification email is (re)sent per the
|
|
361
|
+
* `emailVerification` settings (`sendOnSignUp` / `sendOnSignIn`).
|
|
362
|
+
*
|
|
363
|
+
* The gate checks the local user's verification state, not the provider's
|
|
364
|
+
* claim on each request: a user already verified through another method (or a
|
|
365
|
+
* prior verified sign-in) keeps access even if the provider later reports the
|
|
366
|
+
* email as unverified.
|
|
367
|
+
*
|
|
368
|
+
* This is opt-in per provider and is independent of
|
|
369
|
+
* `emailAndPassword.requireEmailVerification`; enabling that does not gate
|
|
370
|
+
* social sign-in. Only enable it for providers that report a trustworthy
|
|
371
|
+
* `email_verified` signal: several providers always report the email as
|
|
372
|
+
* unverified, which would block every sign-in.
|
|
373
|
+
*
|
|
374
|
+
* @default false
|
|
375
|
+
*/
|
|
376
|
+
requireEmailVerification?: boolean | undefined;
|
|
247
377
|
};
|
|
@@ -119,7 +119,7 @@ export async function refreshAccessToken({
|
|
|
119
119
|
expires_in?: number | undefined;
|
|
120
120
|
refresh_token_expires_in?: number | undefined;
|
|
121
121
|
token_type?: string | undefined;
|
|
122
|
-
scope?: string | undefined;
|
|
122
|
+
scope?: (string | string[]) | undefined;
|
|
123
123
|
id_token?: string | undefined;
|
|
124
124
|
}>(tokenEndpoint, {
|
|
125
125
|
method: "POST",
|
|
@@ -133,7 +133,7 @@ export async function refreshAccessToken({
|
|
|
133
133
|
accessToken: data.access_token,
|
|
134
134
|
refreshToken: data.refresh_token,
|
|
135
135
|
tokenType: data.token_type,
|
|
136
|
-
scopes: data.scope?.split(" "),
|
|
136
|
+
scopes: Array.isArray(data.scope) ? data.scope : data.scope?.split(" "),
|
|
137
137
|
idToken: data.id_token,
|
|
138
138
|
};
|
|
139
139
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ProviderOptions } from "./oauth-provider";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a provider's `scope` token-response field into a string array.
|
|
5
|
+
*
|
|
6
|
+
* RFC 6749 §3.3 defines `scope` as a space-delimited string, but providers
|
|
7
|
+
* vary: some (e.g. Twitch) return an already-split array. Accept both, plus the
|
|
8
|
+
* omitted/empty case, without ever calling `.split` on a non-string. Returns
|
|
9
|
+
* `[]` when no scope is present.
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/better-auth/better-auth/issues/9076
|
|
12
|
+
*/
|
|
13
|
+
export function parseScopeField(scope: unknown): string[] {
|
|
14
|
+
if (Array.isArray(scope))
|
|
15
|
+
return scope.filter((s): s is string => typeof s === "string" && s !== "");
|
|
16
|
+
if (typeof scope === "string") return scope.split(" ").filter(Boolean);
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a scope set into a single deduped, sorted array.
|
|
22
|
+
*
|
|
23
|
+
* Scope order is insignificant per RFC 6749 §3.3, so normalize for idempotent
|
|
24
|
+
* writes and trivial comparisons: trim each token, drop empties, dedupe, and
|
|
25
|
+
* sort ascending. Returns `[]` when the union is empty.
|
|
26
|
+
*
|
|
27
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeScopes(
|
|
30
|
+
stored: string[] | null | undefined,
|
|
31
|
+
incoming?: string[] | undefined,
|
|
32
|
+
): string[] {
|
|
33
|
+
const normalized = new Set<string>();
|
|
34
|
+
for (const scope of [...(stored ?? []), ...(incoming ?? [])]) {
|
|
35
|
+
const trimmed = scope.trim();
|
|
36
|
+
if (trimmed) normalized.add(trimmed);
|
|
37
|
+
}
|
|
38
|
+
return [...normalized].sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Union the stored granted-scope set with the scopes observed on an
|
|
43
|
+
* authorization or token exchange.
|
|
44
|
+
*
|
|
45
|
+
* The provider's echoed `scope` is authoritative when present. RFC 6749 §3.3
|
|
46
|
+
* and §5.1 say an omitted or empty echo means the grant equals what was
|
|
47
|
+
* requested, so fall back to `requested` in that case. The result unions onto
|
|
48
|
+
* the stored grant (never narrows on a normal write) and is normalized per
|
|
49
|
+
* {@link normalizeScopes}.
|
|
50
|
+
*
|
|
51
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
|
|
52
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
|
|
53
|
+
*/
|
|
54
|
+
export function unionGrantedScopes(
|
|
55
|
+
stored: string[] | null | undefined,
|
|
56
|
+
echoed: string[] | undefined,
|
|
57
|
+
requested: string[] | undefined,
|
|
58
|
+
): string[] {
|
|
59
|
+
const granted = echoed?.length ? echoed : requested;
|
|
60
|
+
return normalizeScopes(stored, granted);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Coerce a stored granted-scope value into a usable array.
|
|
65
|
+
*
|
|
66
|
+
* `account.grantedScopes` is nullable (legacy rows and non-OAuth accounts read
|
|
67
|
+
* as unset), and on dialects that store the array as a JSON string a malformed
|
|
68
|
+
* operator backfill could deserialize to a non-array. Both collapse to `[]`
|
|
69
|
+
* here so every reader works against a real `string[]` without re-deriving the
|
|
70
|
+
* guard.
|
|
71
|
+
*/
|
|
72
|
+
export function readGrantedScopes(
|
|
73
|
+
stored: string[] | null | undefined,
|
|
74
|
+
): string[] {
|
|
75
|
+
return Array.isArray(stored) ? stored : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Test whether a normalized granted-scope set contains a specific scope.
|
|
80
|
+
*
|
|
81
|
+
* Matching is exact and case-sensitive per RFC 6749 §3.3. The argument is the
|
|
82
|
+
* normalized `account.grantedScopes` array; a raw provider `scope` string must
|
|
83
|
+
* be run through {@link parseScopeField} first.
|
|
84
|
+
*
|
|
85
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-3.3
|
|
86
|
+
*/
|
|
87
|
+
export function includesGrantedScope(
|
|
88
|
+
granted: string[] | null | undefined,
|
|
89
|
+
scope: string,
|
|
90
|
+
): boolean {
|
|
91
|
+
return granted?.includes(scope) ?? false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compose the effective scope set to encode in a single authorization URL.
|
|
96
|
+
*
|
|
97
|
+
* Precedence: the provider's built-in defaults (unless `disableDefaultScope`),
|
|
98
|
+
* then the integrator's configured `options.scope`, then the per-request
|
|
99
|
+
* `scopes`. The result is the value persisted into OAuth state as the RFC 6749
|
|
100
|
+
* §5.1 fallback, so it is preserved verbatim (not normalized) to match what is
|
|
101
|
+
* sent to the provider.
|
|
102
|
+
*
|
|
103
|
+
* `defaultScopes` is a parameter rather than a provider-contract field so the
|
|
104
|
+
* runtime-synthesized generic OAuth provider, which has no static default set,
|
|
105
|
+
* can pass its configured scopes here.
|
|
106
|
+
*
|
|
107
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
|
|
108
|
+
*/
|
|
109
|
+
export function resolveRequestedScopes(
|
|
110
|
+
options: Pick<ProviderOptions, "scope" | "disableDefaultScope"> | undefined,
|
|
111
|
+
defaultScopes: string[],
|
|
112
|
+
perRequestScopes: string[] | undefined,
|
|
113
|
+
): string[] {
|
|
114
|
+
const scopes = options?.disableDefaultScope ? [] : [...defaultScopes];
|
|
115
|
+
if (options?.scope) scopes.push(...options.scope);
|
|
116
|
+
if (perRequestScopes) scopes.push(...perRequestScopes);
|
|
117
|
+
return scopes;
|
|
118
|
+
}
|
package/src/oauth2/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { base64Url } from "@better-auth/utils/base64";
|
|
2
2
|
import type { OAuth2Tokens } from "./oauth-provider";
|
|
3
|
+
import { parseScopeField } from "./scopes";
|
|
3
4
|
|
|
4
5
|
export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
5
6
|
const getDate = (seconds: number) => {
|
|
@@ -17,11 +18,7 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
|
17
18
|
refreshTokenExpiresAt: data.refresh_token_expires_in
|
|
18
19
|
? getDate(data.refresh_token_expires_in)
|
|
19
20
|
: undefined,
|
|
20
|
-
scopes: data
|
|
21
|
-
? typeof data.scope === "string"
|
|
22
|
-
? data.scope.split(" ")
|
|
23
|
-
: data.scope
|
|
24
|
-
: [],
|
|
21
|
+
scopes: parseScopeField(data.scope),
|
|
25
22
|
idToken: data.id_token,
|
|
26
23
|
// Preserve the raw token response for provider-specific fields
|
|
27
24
|
raw: data,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { decodeProtectedHeader, jwtVerify } from "jose";
|
|
2
|
+
import type { ProviderOptions, UpstreamProvider } from "./oauth-provider";
|
|
3
|
+
|
|
4
|
+
async function sha256Hex(value: string) {
|
|
5
|
+
const data = new TextEncoder().encode(value);
|
|
6
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
7
|
+
return Array.from(new Uint8Array(digest))
|
|
8
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function nonceMatches(
|
|
13
|
+
claimNonce: unknown,
|
|
14
|
+
nonce: string,
|
|
15
|
+
comparison: "exact" | "exact-or-sha256" = "exact",
|
|
16
|
+
) {
|
|
17
|
+
if (typeof claimNonce !== "string") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (claimNonce === nonce) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (comparison === "exact-or-sha256") {
|
|
24
|
+
return claimNonce === (await sha256Hex(nonce));
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether a provider can verify a client-submitted id_token.
|
|
31
|
+
*
|
|
32
|
+
* A provider supports id_token sign-in when it declares an {@link UpstreamProvider.idToken}
|
|
33
|
+
* verification config, or when the integrator supplies a `verifyIdToken` override on the
|
|
34
|
+
* provider options. A provider whose options set `disableIdTokenSignIn`, or that declares
|
|
35
|
+
* neither, rejects the client id_token sign-in path with `ID_TOKEN_NOT_SUPPORTED`.
|
|
36
|
+
*/
|
|
37
|
+
export function supportsIdTokenSignIn(provider: UpstreamProvider<any, any>) {
|
|
38
|
+
const options = (provider.options ?? {}) as Partial<ProviderOptions>;
|
|
39
|
+
if (options.disableIdTokenSignIn) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return Boolean(provider.idToken || options.verifyIdToken);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Verify a client-submitted id_token against a provider's verification config.
|
|
47
|
+
*
|
|
48
|
+
* This is the single id_token verifier for every social provider. Providers no longer
|
|
49
|
+
* implement their own boolean `verifyIdToken`; they declare an {@link UpstreamProvider.idToken}
|
|
50
|
+
* config and this function performs the cryptographic check. The contract is fail-closed: a
|
|
51
|
+
* provider without a config (and without an integrator `verifyIdToken` override) returns
|
|
52
|
+
* `false`, so a forged token can never be accepted by omission.
|
|
53
|
+
*
|
|
54
|
+
* @returns `true` only when the token is authentic for the provider.
|
|
55
|
+
*/
|
|
56
|
+
export async function verifyProviderIdToken(
|
|
57
|
+
provider: UpstreamProvider<any, any>,
|
|
58
|
+
token: string,
|
|
59
|
+
nonce?: string,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
const options = (provider.options ?? {}) as Partial<ProviderOptions>;
|
|
62
|
+
if (options.disableIdTokenSignIn) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// Every verification path is fail-closed: a throw from the integrator override, a custom
|
|
66
|
+
// remote verifier, the JWKS resolver, or signature checking resolves to `false` instead of
|
|
67
|
+
// escaping to the caller as a server error.
|
|
68
|
+
try {
|
|
69
|
+
if (options.verifyIdToken) {
|
|
70
|
+
return await options.verifyIdToken(token, nonce);
|
|
71
|
+
}
|
|
72
|
+
const config = provider.idToken;
|
|
73
|
+
if (!config) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if ("verify" in config) {
|
|
77
|
+
return await config.verify(token, nonce);
|
|
78
|
+
}
|
|
79
|
+
// Opaque (non-JWS) tokens carry no signature to check. They are accepted only when the
|
|
80
|
+
// provider opts in, in which case getUserInfo resolves identity from the access token via
|
|
81
|
+
// the provider's userinfo endpoint, which validates it (e.g. Facebook Graph access tokens).
|
|
82
|
+
if (token.split(".").length !== 3) {
|
|
83
|
+
return config.allowOpaqueToken === true;
|
|
84
|
+
}
|
|
85
|
+
// `kid` is optional in JWS: a JWKS resolver can still select a key by algorithm, so
|
|
86
|
+
// key selection is left to config.jwks. The token's `alg` only seeds the default
|
|
87
|
+
// allowed-algorithms list when the provider does not pin one.
|
|
88
|
+
const { alg } = decodeProtectedHeader(token);
|
|
89
|
+
const { payload } = await jwtVerify(token, config.jwks, {
|
|
90
|
+
issuer: config.issuer,
|
|
91
|
+
audience: config.audience,
|
|
92
|
+
algorithms: config.algorithms ?? (alg ? [alg] : undefined),
|
|
93
|
+
maxTokenAge: config.maxTokenAge,
|
|
94
|
+
});
|
|
95
|
+
if (
|
|
96
|
+
nonce &&
|
|
97
|
+
!(await nonceMatches(payload.nonce, nonce, config.nonceComparison))
|
|
98
|
+
) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// Provider-specific claim check on the now-verified payload (e.g. Google's
|
|
102
|
+
// hosted-domain `hd`). Standard checks have already passed, so a token that
|
|
103
|
+
// fails here is authentic but does not meet the provider's extra constraint.
|
|
104
|
+
if (config.verifyClaims && !config.verifyClaims(payload)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|