@better-auth/core 1.7.0-beta.1 → 1.7.0-beta.10
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 +69 -4
- 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 +91 -3
- package/dist/db/adapter/get-id-field.mjs +1 -1
- package/dist/db/adapter/index.d.mts +87 -3
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/get-tables.mjs +2 -1
- package/dist/db/type.d.mts +17 -0
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +1 -0
- package/dist/error/codes.mjs +1 -0
- package/dist/error/index.d.mts +7 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/api.mjs +12 -0
- package/dist/instrumentation/noop.mjs +42 -0
- package/dist/instrumentation/pure.index.d.mts +7 -0
- package/dist/instrumentation/pure.index.mjs +7 -0
- package/dist/instrumentation/tracer.mjs +6 -3
- package/dist/oauth2/authorization-params.d.mts +12 -0
- package/dist/oauth2/authorization-params.mjs +12 -0
- package/dist/oauth2/basic-credentials.d.mts +30 -0
- package/dist/oauth2/basic-credentials.mjs +64 -0
- package/dist/oauth2/client-assertion.d.mts +38 -22
- package/dist/oauth2/client-assertion.mjs +63 -28
- package/dist/oauth2/client-credentials-token.d.mts +19 -40
- package/dist/oauth2/client-credentials-token.mjs +20 -31
- package/dist/oauth2/create-authorization-url.d.mts +11 -1
- package/dist/oauth2/create-authorization-url.mjs +27 -7
- package/dist/oauth2/dpop.d.mts +142 -0
- package/dist/oauth2/dpop.mjs +246 -0
- package/dist/oauth2/index.d.mts +14 -9
- package/dist/oauth2/index.mjs +12 -8
- package/dist/oauth2/oauth-provider.d.mts +144 -10
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +38 -36
- package/dist/oauth2/reject-redirects.mjs +65 -0
- package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
- package/dist/oauth2/token-endpoint-auth.mjs +89 -0
- package/dist/oauth2/utils.d.mts +23 -1
- package/dist/oauth2/utils.mjs +48 -2
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +28 -34
- package/dist/oauth2/verify-id-token.d.mts +27 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +88 -15
- package/dist/oauth2/verify.mjs +203 -25
- package/dist/social-providers/apple.d.mts +16 -4
- package/dist/social-providers/apple.mjs +17 -24
- package/dist/social-providers/atlassian.d.mts +5 -1
- package/dist/social-providers/atlassian.mjs +6 -3
- package/dist/social-providers/cognito.d.mts +27 -3
- package/dist/social-providers/cognito.mjs +15 -25
- package/dist/social-providers/discord.d.mts +7 -3
- package/dist/social-providers/discord.mjs +16 -3
- package/dist/social-providers/dropbox.d.mts +5 -1
- package/dist/social-providers/dropbox.mjs +5 -4
- package/dist/social-providers/facebook.d.mts +24 -5
- package/dist/social-providers/facebook.mjs +54 -20
- package/dist/social-providers/figma.d.mts +5 -1
- package/dist/social-providers/figma.mjs +4 -3
- package/dist/social-providers/github.d.mts +6 -2
- package/dist/social-providers/github.mjs +5 -4
- package/dist/social-providers/gitlab.d.mts +5 -1
- package/dist/social-providers/gitlab.mjs +3 -2
- package/dist/social-providers/google.d.mts +53 -5
- package/dist/social-providers/google.mjs +69 -25
- package/dist/social-providers/huggingface.d.mts +5 -1
- package/dist/social-providers/huggingface.mjs +3 -2
- package/dist/social-providers/index.d.mts +221 -45
- package/dist/social-providers/index.mjs +2 -2
- package/dist/social-providers/kakao.d.mts +5 -1
- package/dist/social-providers/kakao.mjs +3 -2
- package/dist/social-providers/kick.d.mts +5 -1
- package/dist/social-providers/kick.mjs +3 -2
- package/dist/social-providers/line.d.mts +8 -2
- package/dist/social-providers/line.mjs +5 -6
- package/dist/social-providers/linear.d.mts +5 -1
- package/dist/social-providers/linear.mjs +3 -2
- package/dist/social-providers/linkedin.d.mts +7 -3
- package/dist/social-providers/linkedin.mjs +4 -3
- package/dist/social-providers/microsoft-entra-id.d.mts +41 -7
- package/dist/social-providers/microsoft-entra-id.mjs +37 -28
- package/dist/social-providers/naver.d.mts +5 -1
- package/dist/social-providers/naver.mjs +3 -2
- package/dist/social-providers/notion.d.mts +5 -1
- package/dist/social-providers/notion.mjs +5 -2
- package/dist/social-providers/paybin.d.mts +5 -1
- package/dist/social-providers/paybin.mjs +4 -3
- package/dist/social-providers/paypal.d.mts +6 -2
- package/dist/social-providers/paypal.mjs +19 -14
- package/dist/social-providers/polar.d.mts +5 -1
- package/dist/social-providers/polar.mjs +3 -2
- package/dist/social-providers/railway.d.mts +5 -1
- package/dist/social-providers/railway.mjs +3 -2
- package/dist/social-providers/reddit.d.mts +5 -1
- package/dist/social-providers/reddit.mjs +7 -5
- package/dist/social-providers/roblox.d.mts +6 -2
- package/dist/social-providers/roblox.mjs +12 -2
- package/dist/social-providers/salesforce.d.mts +5 -1
- package/dist/social-providers/salesforce.mjs +4 -3
- package/dist/social-providers/slack.d.mts +6 -2
- package/dist/social-providers/slack.mjs +11 -8
- package/dist/social-providers/spotify.d.mts +5 -1
- package/dist/social-providers/spotify.mjs +3 -2
- package/dist/social-providers/tiktok.d.mts +5 -1
- package/dist/social-providers/tiktok.mjs +14 -2
- package/dist/social-providers/twitch.d.mts +5 -1
- package/dist/social-providers/twitch.mjs +3 -2
- package/dist/social-providers/twitter.d.mts +4 -1
- package/dist/social-providers/twitter.mjs +2 -1
- package/dist/social-providers/vercel.d.mts +5 -1
- package/dist/social-providers/vercel.mjs +3 -2
- package/dist/social-providers/vk.d.mts +5 -1
- package/dist/social-providers/vk.mjs +3 -2
- package/dist/social-providers/wechat.d.mts +5 -1
- package/dist/social-providers/wechat.mjs +8 -2
- package/dist/social-providers/zoom.d.mts +6 -2
- package/dist/social-providers/zoom.mjs +10 -17
- package/dist/types/context.d.mts +55 -6
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +182 -12
- package/dist/types/plugin-client.d.mts +12 -2
- package/dist/utils/async.d.mts +22 -0
- package/dist/utils/async.mjs +32 -0
- package/dist/utils/host.d.mts +147 -0
- package/dist/utils/host.mjs +298 -0
- package/dist/utils/ip.d.mts +28 -5
- package/dist/utils/ip.mjs +118 -4
- package/dist/utils/is-api-error.d.mts +6 -0
- package/dist/utils/is-api-error.mjs +8 -0
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +34 -4
- package/package.json +26 -16
- package/src/api/index.ts +121 -5
- package/src/context/transaction.ts +45 -12
- package/src/db/adapter/factory.ts +193 -7
- package/src/db/adapter/get-id-field.ts +2 -2
- package/src/db/adapter/index.ts +85 -2
- package/src/db/adapter/types.ts +2 -0
- package/src/db/get-tables.ts +9 -1
- package/src/db/schema/account.ts +4 -1
- package/src/db/schema/user.ts +3 -0
- package/src/db/type.ts +17 -0
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +1 -0
- package/src/error/index.ts +9 -0
- package/src/instrumentation/api.ts +17 -0
- package/src/instrumentation/noop.ts +74 -0
- package/src/instrumentation/pure.index.ts +31 -0
- package/src/instrumentation/tracer.ts +8 -3
- package/src/oauth2/authorization-params.ts +28 -0
- package/src/oauth2/basic-credentials.ts +87 -0
- package/src/oauth2/client-assertion.ts +131 -58
- package/src/oauth2/client-credentials-token.ts +50 -74
- package/src/oauth2/create-authorization-url.ts +33 -7
- package/src/oauth2/dpop.ts +568 -0
- package/src/oauth2/index.ts +81 -11
- package/src/oauth2/oauth-provider.ts +153 -11
- package/src/oauth2/refresh-access-token.ts +82 -83
- package/src/oauth2/reject-redirects.ts +75 -0
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +72 -5
- package/src/oauth2/validate-authorization-code.ts +69 -89
- package/src/oauth2/verify-id-token.ts +115 -0
- package/src/oauth2/verify.ts +409 -78
- package/src/social-providers/apple.ts +30 -40
- package/src/social-providers/atlassian.ts +8 -1
- package/src/social-providers/cognito.ts +36 -39
- package/src/social-providers/discord.ts +22 -18
- package/src/social-providers/dropbox.ts +7 -5
- package/src/social-providers/facebook.ts +108 -52
- package/src/social-providers/figma.ts +8 -1
- package/src/social-providers/github.ts +5 -3
- package/src/social-providers/gitlab.ts +2 -0
- package/src/social-providers/google.ts +141 -39
- package/src/social-providers/huggingface.ts +8 -1
- package/src/social-providers/kakao.ts +2 -1
- package/src/social-providers/kick.ts +8 -1
- package/src/social-providers/line.ts +25 -27
- package/src/social-providers/linear.ts +8 -1
- package/src/social-providers/linkedin.ts +5 -3
- package/src/social-providers/microsoft-entra-id.ts +100 -55
- package/src/social-providers/naver.ts +2 -1
- package/src/social-providers/notion.ts +8 -1
- package/src/social-providers/paybin.ts +2 -0
- package/src/social-providers/paypal.ts +28 -17
- package/src/social-providers/polar.ts +8 -1
- package/src/social-providers/railway.ts +8 -1
- package/src/social-providers/reddit.ts +9 -4
- package/src/social-providers/roblox.ts +16 -11
- package/src/social-providers/salesforce.ts +8 -1
- package/src/social-providers/slack.ts +15 -9
- package/src/social-providers/spotify.ts +8 -1
- package/src/social-providers/tiktok.ts +22 -9
- package/src/social-providers/twitch.ts +2 -1
- package/src/social-providers/twitter.ts +1 -0
- package/src/social-providers/vercel.ts +8 -1
- package/src/social-providers/vk.ts +8 -1
- package/src/social-providers/wechat.ts +17 -2
- package/src/social-providers/zoom.ts +15 -19
- package/src/types/context.ts +57 -5
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +204 -14
- package/src/types/plugin-client.ts +16 -2
- package/src/utils/async.ts +53 -0
- package/src/utils/host.ts +425 -0
- package/src/utils/ip.ts +197 -13
- package/src/utils/is-api-error.ts +10 -0
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +38 -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,34 @@ export type OAuth2UserInfo = {
|
|
|
23
77
|
emailVerified: boolean;
|
|
24
78
|
};
|
|
25
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Request metadata available to provider refresh hooks.
|
|
82
|
+
*
|
|
83
|
+
* The refresh flow may be triggered by endpoints such as `getAccessToken` or
|
|
84
|
+
* `refreshToken`; this context gives provider hooks access to the triggering
|
|
85
|
+
* request without exposing the full endpoint implementation surface.
|
|
86
|
+
*/
|
|
87
|
+
export interface OAuthRefreshContext {
|
|
88
|
+
headers?: Headers | undefined;
|
|
89
|
+
request?: Request | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
26
92
|
export interface OAuthProvider<
|
|
27
93
|
T extends Record<string, any> = Record<string, any>,
|
|
28
94
|
O extends Record<string, any> = Partial<ProviderOptions>,
|
|
29
95
|
> {
|
|
30
96
|
id: LiteralString;
|
|
97
|
+
/**
|
|
98
|
+
* Optional path under the resolved per-request `baseURL` where this
|
|
99
|
+
* provider's OAuth callback handler is mounted. Providers that use the
|
|
100
|
+
* shared `/callback/<id>` route can omit this.
|
|
101
|
+
*
|
|
102
|
+
* Custom paths must start with `/`.
|
|
103
|
+
*
|
|
104
|
+
* Endpoints compose `redirectURI = ctx.context.baseURL + callbackPath` per
|
|
105
|
+
* request, so the provider must not hardcode an origin or `baseURL` here.
|
|
106
|
+
*/
|
|
107
|
+
callbackPath?: string | undefined;
|
|
31
108
|
createAuthorizationURL: (data: {
|
|
32
109
|
state: string;
|
|
33
110
|
codeVerifier: string;
|
|
@@ -35,6 +112,19 @@ export interface OAuthProvider<
|
|
|
35
112
|
redirectURI: string;
|
|
36
113
|
display?: string | undefined;
|
|
37
114
|
loginHint?: string | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* OIDC nonce generated by the redirect initiator and persisted in OAuth
|
|
117
|
+
* state. Providers that set `requiresIdTokenNonce` must forward this to
|
|
118
|
+
* the authorization URL as the `nonce` parameter.
|
|
119
|
+
*/
|
|
120
|
+
idTokenNonce?: string | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* Extra query parameters to append to the authorization URL.
|
|
123
|
+
* Providers forward these to the shared `createAuthorizationURL` helper,
|
|
124
|
+
* which drops any keys present in `RESERVED_AUTHORIZATION_PARAMS`
|
|
125
|
+
* before applying them.
|
|
126
|
+
*/
|
|
127
|
+
additionalParams?: Record<string, string> | undefined;
|
|
38
128
|
}) => Awaitable<URL>;
|
|
39
129
|
name: string;
|
|
40
130
|
validateAuthorizationCode: (data: {
|
|
@@ -45,6 +135,12 @@ export interface OAuthProvider<
|
|
|
45
135
|
}) => Promise<OAuth2Tokens | null>;
|
|
46
136
|
getUserInfo: (
|
|
47
137
|
token: OAuth2Tokens & {
|
|
138
|
+
/**
|
|
139
|
+
* OIDC nonce recovered from OAuth state. Providers that required an
|
|
140
|
+
* ID-token nonce must pass this to `verifyProviderIdToken` before
|
|
141
|
+
* trusting ID-token claims.
|
|
142
|
+
*/
|
|
143
|
+
expectedIdTokenNonce?: string | undefined;
|
|
48
144
|
/**
|
|
49
145
|
* The user object from the provider
|
|
50
146
|
* This is only available for some providers like Apple
|
|
@@ -64,27 +160,38 @@ export interface OAuthProvider<
|
|
|
64
160
|
data: T;
|
|
65
161
|
} | null>;
|
|
66
162
|
/**
|
|
67
|
-
* Custom function to refresh a token
|
|
163
|
+
* Custom function to refresh a token.
|
|
164
|
+
*
|
|
165
|
+
* Receives request metadata from the endpoint that triggered the refresh.
|
|
166
|
+
* Providers that don't need request-scoped data can ignore the second
|
|
167
|
+
* argument.
|
|
68
168
|
*/
|
|
69
169
|
refreshAccessToken?:
|
|
70
|
-
| ((
|
|
170
|
+
| ((
|
|
171
|
+
refreshToken: string,
|
|
172
|
+
ctx?: OAuthRefreshContext,
|
|
173
|
+
) => Promise<OAuth2Tokens>)
|
|
71
174
|
| undefined;
|
|
72
175
|
revokeToken?: ((token: string) => Promise<void>) | undefined;
|
|
73
176
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* @returns True if the id token is valid, false otherwise
|
|
177
|
+
* Declarative id_token verification config consumed by the shared
|
|
178
|
+
* `verifyProviderIdToken` verifier. Providers set this instead of implementing a boolean
|
|
179
|
+
* verify method, which keeps verification centralized and fail-closed.
|
|
78
180
|
*/
|
|
79
|
-
|
|
80
|
-
| ((token: string, nonce?: string) => Promise<boolean>)
|
|
81
|
-
| undefined;
|
|
181
|
+
idToken?: OAuthIdTokenConfig | undefined;
|
|
82
182
|
/**
|
|
83
183
|
* The expected issuer identifier for this provider (RFC 9207).
|
|
84
184
|
* When set, the callback handler validates the `iss` query parameter
|
|
85
185
|
* against this value to prevent authorization server mix-up attacks.
|
|
86
186
|
*/
|
|
87
187
|
issuer?: string | undefined;
|
|
188
|
+
/**
|
|
189
|
+
* Require shared OAuth redirect routes to bind ID-token verification to an
|
|
190
|
+
* authorization request nonce. When true, routes generate `idTokenNonce`,
|
|
191
|
+
* pass it to `createAuthorizationURL`, persist it in state, and provide it
|
|
192
|
+
* back to `getUserInfo` as `expectedIdTokenNonce`.
|
|
193
|
+
*/
|
|
194
|
+
requiresIdTokenNonce?: boolean | undefined;
|
|
88
195
|
/**
|
|
89
196
|
* Disable implicit sign up for new users. When set to true for the provider,
|
|
90
197
|
* sign-in need to be called with with requestSignUp as true to create new users.
|
|
@@ -94,6 +201,17 @@ export interface OAuthProvider<
|
|
|
94
201
|
* Disable sign up for new users.
|
|
95
202
|
*/
|
|
96
203
|
disableSignUp?: boolean | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* Accept callbacks that arrive without a `state` parameter. When true,
|
|
206
|
+
* the shared OAuth callback handler restarts the flow server-side with
|
|
207
|
+
* fresh `state` and PKCE instead of rejecting the request. Intended for
|
|
208
|
+
* providers that initiate OAuth without RP-side flow kickoff (e.g.
|
|
209
|
+
* Clever). Leave unset for any provider that always initiates from the
|
|
210
|
+
* RP.
|
|
211
|
+
*
|
|
212
|
+
* @default false
|
|
213
|
+
*/
|
|
214
|
+
allowIdpInitiated?: boolean | undefined;
|
|
97
215
|
/**
|
|
98
216
|
* Options for the provider
|
|
99
217
|
*/
|
|
@@ -104,9 +222,10 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
104
222
|
/**
|
|
105
223
|
* The client ID of your application.
|
|
106
224
|
*
|
|
107
|
-
*
|
|
225
|
+
* Some providers accept multiple platform client IDs. The first entry is the
|
|
226
|
+
* primary client ID used for token endpoint client authentication.
|
|
108
227
|
*/
|
|
109
|
-
clientId?:
|
|
228
|
+
clientId?: LiteralString | string[] | undefined;
|
|
110
229
|
/**
|
|
111
230
|
* The client secret of your application
|
|
112
231
|
*/
|
|
@@ -225,4 +344,27 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
|
|
|
225
344
|
* @default false
|
|
226
345
|
*/
|
|
227
346
|
overrideUserInfoOnSignIn?: boolean | undefined;
|
|
347
|
+
/**
|
|
348
|
+
* Require this provider's email to be verified before a session is created.
|
|
349
|
+
*
|
|
350
|
+
* When the provider reports the email as unverified, the user and account are
|
|
351
|
+
* still created/linked, but no session is issued: the OAuth callback redirects
|
|
352
|
+
* with `?error=email_not_verified` and id-token sign-in returns a `403`
|
|
353
|
+
* `EMAIL_NOT_VERIFIED`. A verification email is (re)sent per the
|
|
354
|
+
* `emailVerification` settings (`sendOnSignUp` / `sendOnSignIn`).
|
|
355
|
+
*
|
|
356
|
+
* The gate checks the local user's verification state, not the provider's
|
|
357
|
+
* claim on each request: a user already verified through another method (or a
|
|
358
|
+
* prior verified sign-in) keeps access even if the provider later reports the
|
|
359
|
+
* email as unverified.
|
|
360
|
+
*
|
|
361
|
+
* This is opt-in per provider and is independent of
|
|
362
|
+
* `emailAndPassword.requireEmailVerification`; enabling that does not gate
|
|
363
|
+
* social sign-in. Only enable it for providers that report a trustworthy
|
|
364
|
+
* `email_verified` signal: several providers always report the email as
|
|
365
|
+
* unverified, which would block every sign-in.
|
|
366
|
+
*
|
|
367
|
+
* @default false
|
|
368
|
+
*/
|
|
369
|
+
requireEmailVerification?: boolean | undefined;
|
|
228
370
|
};
|
|
@@ -1,99 +1,105 @@
|
|
|
1
|
-
import { base64 } from "@better-auth/utils/base64";
|
|
2
|
-
import { betterFetch } from "@better-fetch/fetch";
|
|
3
1
|
import type { AwaitableFunction } from "../types";
|
|
4
|
-
import type { ClientAssertionConfig } from "./client-assertion";
|
|
5
|
-
import { resolveAssertionParams } from "./client-assertion";
|
|
6
2
|
import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider";
|
|
3
|
+
import { fetchRefusingRedirects } from "./reject-redirects";
|
|
4
|
+
import type {
|
|
5
|
+
TokenEndpointAuth,
|
|
6
|
+
TokenEndpointSecretAuthentication,
|
|
7
|
+
} from "./token-endpoint-auth";
|
|
8
|
+
import { applyTokenEndpointAuth } from "./token-endpoint-auth";
|
|
9
|
+
import { parseScopeField } from "./utils";
|
|
10
|
+
|
|
11
|
+
interface RefreshAccessTokenRequestInput {
|
|
12
|
+
refreshToken: string;
|
|
13
|
+
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
14
|
+
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
15
|
+
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
16
|
+
tokenEndpoint?: string | undefined;
|
|
17
|
+
extraParams?: Record<string, string> | undefined;
|
|
18
|
+
resource?: (string | string[]) | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RefreshAccessTokenRequestBaseInput {
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
options: ProviderOptions;
|
|
24
|
+
extraParams?: Record<string, string> | undefined;
|
|
25
|
+
resource?: (string | string[]) | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RefreshAccessTokenInput extends RefreshAccessTokenRequestInput {
|
|
29
|
+
options: Partial<ProviderOptions>;
|
|
30
|
+
tokenEndpoint: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Body keys owned by the refresh-token flow or unsafe to copy from caller input.
|
|
35
|
+
*/
|
|
36
|
+
const BLOCKED_REFRESH_TOKEN_PARAMS = [
|
|
37
|
+
"grant_type",
|
|
38
|
+
"refresh_token",
|
|
39
|
+
"__proto__",
|
|
40
|
+
"constructor",
|
|
41
|
+
"prototype",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
const BLOCKED_REFRESH_TOKEN_PARAMS_SET: ReadonlySet<string> = new Set(
|
|
45
|
+
BLOCKED_REFRESH_TOKEN_PARAMS,
|
|
46
|
+
);
|
|
7
47
|
|
|
8
48
|
export async function refreshAccessTokenRequest({
|
|
9
49
|
refreshToken,
|
|
10
50
|
options,
|
|
11
51
|
authentication,
|
|
12
|
-
|
|
52
|
+
tokenEndpointAuth,
|
|
13
53
|
tokenEndpoint,
|
|
14
54
|
extraParams,
|
|
15
55
|
resource,
|
|
16
|
-
}: {
|
|
17
|
-
refreshToken: string;
|
|
18
|
-
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
19
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
20
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
21
|
-
/** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
|
|
22
|
-
tokenEndpoint?: string | undefined;
|
|
23
|
-
extraParams?: Record<string, string> | undefined;
|
|
24
|
-
resource?: (string | string[]) | undefined;
|
|
25
|
-
}) {
|
|
56
|
+
}: RefreshAccessTokenRequestInput) {
|
|
26
57
|
options = typeof options === "function" ? await options() : options;
|
|
27
|
-
|
|
28
|
-
if (authentication === "private_key_jwt") {
|
|
29
|
-
if (!clientAssertion) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
"private_key_jwt authentication requires a clientAssertion configuration",
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
const primaryClientId = Array.isArray(options.clientId)
|
|
35
|
-
? options.clientId[0]
|
|
36
|
-
: options.clientId;
|
|
37
|
-
const assertionParams = await resolveAssertionParams({
|
|
38
|
-
clientAssertion,
|
|
39
|
-
clientId: primaryClientId,
|
|
40
|
-
tokenEndpoint,
|
|
41
|
-
});
|
|
42
|
-
extraParams = { ...extraParams, ...assertionParams };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return createRefreshAccessTokenRequest({
|
|
58
|
+
const request = buildRefreshAccessTokenRequest({
|
|
46
59
|
refreshToken,
|
|
47
60
|
options,
|
|
48
|
-
authentication,
|
|
49
61
|
extraParams,
|
|
50
62
|
resource,
|
|
51
63
|
});
|
|
64
|
+
|
|
65
|
+
await applyTokenEndpointAuth({
|
|
66
|
+
body: request.body,
|
|
67
|
+
headers: request.headers,
|
|
68
|
+
options,
|
|
69
|
+
tokenEndpoint: tokenEndpoint ?? "",
|
|
70
|
+
grantType: "refresh_token",
|
|
71
|
+
tokenEndpointAuth,
|
|
72
|
+
authentication,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return request;
|
|
52
76
|
}
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
function applyRefreshExtraParams(
|
|
79
|
+
body: URLSearchParams,
|
|
80
|
+
extraParams: Record<string, string> | undefined,
|
|
81
|
+
) {
|
|
82
|
+
if (!extraParams) return;
|
|
83
|
+
for (const [key, value] of Object.entries(extraParams)) {
|
|
84
|
+
if (BLOCKED_REFRESH_TOKEN_PARAMS_SET.has(key)) continue;
|
|
85
|
+
body.set(key, value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildRefreshAccessTokenRequest({
|
|
58
90
|
refreshToken,
|
|
59
91
|
options,
|
|
60
|
-
authentication,
|
|
61
92
|
extraParams,
|
|
62
93
|
resource,
|
|
63
|
-
}: {
|
|
64
|
-
refreshToken: string;
|
|
65
|
-
options: ProviderOptions;
|
|
66
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
67
|
-
extraParams?: Record<string, string> | undefined;
|
|
68
|
-
resource?: (string | string[]) | undefined;
|
|
69
|
-
}) {
|
|
94
|
+
}: RefreshAccessTokenRequestBaseInput) {
|
|
70
95
|
const body = new URLSearchParams();
|
|
71
|
-
const headers: Record<string,
|
|
96
|
+
const headers: Record<string, string> = {
|
|
72
97
|
"content-type": "application/x-www-form-urlencoded",
|
|
73
98
|
accept: "application/json",
|
|
74
99
|
};
|
|
75
100
|
|
|
76
101
|
body.set("grant_type", "refresh_token");
|
|
77
102
|
body.set("refresh_token", refreshToken);
|
|
78
|
-
const primaryClientId = Array.isArray(options.clientId)
|
|
79
|
-
? options.clientId[0]
|
|
80
|
-
: options.clientId;
|
|
81
|
-
if (authentication === "basic") {
|
|
82
|
-
if (primaryClientId) {
|
|
83
|
-
headers["authorization"] =
|
|
84
|
-
"Basic " +
|
|
85
|
-
base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
|
|
86
|
-
} else {
|
|
87
|
-
headers["authorization"] =
|
|
88
|
-
"Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
body.set("client_id", primaryClientId);
|
|
92
|
-
if (authentication !== "private_key_jwt" && options.clientSecret) {
|
|
93
|
-
body.set("client_secret", options.clientSecret);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
103
|
if (resource) {
|
|
98
104
|
if (typeof resource === "string") {
|
|
99
105
|
body.append("resource", resource);
|
|
@@ -104,9 +110,7 @@ export function createRefreshAccessTokenRequest({
|
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
if (extraParams) {
|
|
107
|
-
|
|
108
|
-
body.set(key, value);
|
|
109
|
-
}
|
|
113
|
+
applyRefreshExtraParams(body, extraParams);
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
return {
|
|
@@ -120,32 +124,27 @@ export async function refreshAccessToken({
|
|
|
120
124
|
options,
|
|
121
125
|
tokenEndpoint,
|
|
122
126
|
authentication,
|
|
123
|
-
|
|
127
|
+
tokenEndpointAuth,
|
|
124
128
|
extraParams,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
options: Partial<ProviderOptions>;
|
|
128
|
-
tokenEndpoint: string;
|
|
129
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
130
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
131
|
-
extraParams?: Record<string, string> | undefined;
|
|
132
|
-
}): Promise<OAuth2Tokens> {
|
|
129
|
+
resource,
|
|
130
|
+
}: RefreshAccessTokenInput): Promise<OAuth2Tokens> {
|
|
133
131
|
const { body, headers } = await refreshAccessTokenRequest({
|
|
134
132
|
refreshToken,
|
|
135
133
|
options,
|
|
136
134
|
authentication,
|
|
137
|
-
|
|
135
|
+
tokenEndpointAuth,
|
|
138
136
|
tokenEndpoint,
|
|
139
137
|
extraParams,
|
|
138
|
+
resource,
|
|
140
139
|
});
|
|
141
140
|
|
|
142
|
-
const { data, error } = await
|
|
141
|
+
const { data, error } = await fetchRefusingRedirects<{
|
|
143
142
|
access_token: string;
|
|
144
143
|
refresh_token?: string | undefined;
|
|
145
144
|
expires_in?: number | undefined;
|
|
146
145
|
refresh_token_expires_in?: number | undefined;
|
|
147
146
|
token_type?: string | undefined;
|
|
148
|
-
scope?:
|
|
147
|
+
scope?: unknown;
|
|
149
148
|
id_token?: string | undefined;
|
|
150
149
|
}>(tokenEndpoint, {
|
|
151
150
|
method: "POST",
|
|
@@ -159,7 +158,7 @@ export async function refreshAccessToken({
|
|
|
159
158
|
accessToken: data.access_token,
|
|
160
159
|
refreshToken: data.refresh_token,
|
|
161
160
|
tokenType: data.token_type,
|
|
162
|
-
scopes: data.scope
|
|
161
|
+
scopes: parseScopeField(data.scope),
|
|
163
162
|
idToken: data.id_token,
|
|
164
163
|
};
|
|
165
164
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// cspell:ignore workerd
|
|
2
|
+
import type { BetterFetchOption } from "@better-fetch/fetch";
|
|
3
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
4
|
+
import { BetterAuthError } from "../error";
|
|
5
|
+
|
|
6
|
+
const httpRedirectStatuses = new Set([301, 302, 303, 307, 308]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether a response from a `redirect: "manual"` fetch is a redirect.
|
|
10
|
+
*
|
|
11
|
+
* Node/undici exposes the real 3xx status. Spec-compliant runtimes (Cloudflare
|
|
12
|
+
* Workers, Deno, browsers) return an opaque-redirect filtered response with
|
|
13
|
+
* status 0 and type `"opaqueredirect"`, so the status alone is not enough.
|
|
14
|
+
*/
|
|
15
|
+
function isRedirectResponse(response: Response): boolean {
|
|
16
|
+
return (
|
|
17
|
+
response.type === "opaqueredirect" ||
|
|
18
|
+
httpRedirectStatuses.has(response.status)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function redirectRefused(endpoint: string): BetterAuthError {
|
|
23
|
+
return new BetterAuthError(
|
|
24
|
+
`The OAuth endpoint "${endpoint}" returned an HTTP redirect. Server-side OAuth fetches refuse redirects to prevent SSRF; configure the final endpoint URL.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fetch option that refuses HTTP redirects portably.
|
|
30
|
+
*
|
|
31
|
+
* Cloudflare Workers (workerd) rejects `redirect: "error"`, so manual mode is
|
|
32
|
+
* used and the resolved response is checked with {@link assertResponseNotRedirect}
|
|
33
|
+
* (or, for betterFetch, with {@link fetchRefusingRedirects}).
|
|
34
|
+
*/
|
|
35
|
+
export const noFollowRedirect = { redirect: "manual" } as const;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Throw when a native-`fetch` response (e.g. jose's JWKS loader) resolved to a
|
|
39
|
+
* redirect, so an attacker-influenced endpoint cannot bounce a server-side
|
|
40
|
+
* request to an internal address.
|
|
41
|
+
*/
|
|
42
|
+
export function assertResponseNotRedirect(
|
|
43
|
+
endpoint: string,
|
|
44
|
+
response: Response,
|
|
45
|
+
): void {
|
|
46
|
+
if (isRedirectResponse(response)) throw redirectRefused(endpoint);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* betterFetch that refuses HTTP redirects on a server-side OAuth fetch.
|
|
51
|
+
*
|
|
52
|
+
* Returns the betterFetch result and throws if the endpoint redirected, on both
|
|
53
|
+
* undici (real 3xx status) and spec-compliant runtimes (opaque redirect, where
|
|
54
|
+
* the error status is 0). The redirect is never followed on any runtime.
|
|
55
|
+
*/
|
|
56
|
+
export async function fetchRefusingRedirects<T>(
|
|
57
|
+
url: string,
|
|
58
|
+
options?: BetterFetchOption,
|
|
59
|
+
) {
|
|
60
|
+
let redirected = false;
|
|
61
|
+
const onError = options?.onError;
|
|
62
|
+
const result = await betterFetch<T>(url, {
|
|
63
|
+
...options,
|
|
64
|
+
...noFollowRedirect,
|
|
65
|
+
async onError(context) {
|
|
66
|
+
if (isRedirectResponse(context.response)) redirected = true;
|
|
67
|
+
await onError?.(context);
|
|
68
|
+
},
|
|
69
|
+
}).catch((error) => {
|
|
70
|
+
if (redirected) throw redirectRefused(url);
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
if (redirected) throw redirectRefused(url);
|
|
74
|
+
return result;
|
|
75
|
+
}
|