@better-auth/core 1.7.0-beta.0 → 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 +150 -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 +5 -1
- 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 +159 -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,61 +1,41 @@
|
|
|
1
|
-
import { ClientAssertionConfig } from "./client-assertion.mjs";
|
|
2
1
|
import { AwaitableFunction } from "../types/helper.mjs";
|
|
3
2
|
import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
|
|
3
|
+
import { TokenEndpointAuth, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/oauth2/refresh-access-token.d.ts
|
|
6
|
-
|
|
7
|
-
refreshToken,
|
|
8
|
-
options,
|
|
9
|
-
authentication,
|
|
10
|
-
clientAssertion,
|
|
11
|
-
tokenEndpoint,
|
|
12
|
-
extraParams,
|
|
13
|
-
resource
|
|
14
|
-
}: {
|
|
6
|
+
interface RefreshAccessTokenRequestInput {
|
|
15
7
|
refreshToken: string;
|
|
16
8
|
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
17
|
-
authentication?:
|
|
18
|
-
|
|
9
|
+
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
10
|
+
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
19
11
|
tokenEndpoint?: string | undefined;
|
|
20
12
|
extraParams?: Record<string, string> | undefined;
|
|
21
13
|
resource?: (string | string[]) | undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*/
|
|
29
|
-
declare function createRefreshAccessTokenRequest({
|
|
14
|
+
}
|
|
15
|
+
interface RefreshAccessTokenInput extends RefreshAccessTokenRequestInput {
|
|
16
|
+
options: Partial<ProviderOptions>;
|
|
17
|
+
tokenEndpoint: string;
|
|
18
|
+
}
|
|
19
|
+
declare function refreshAccessTokenRequest({
|
|
30
20
|
refreshToken,
|
|
31
21
|
options,
|
|
32
22
|
authentication,
|
|
23
|
+
tokenEndpointAuth,
|
|
24
|
+
tokenEndpoint,
|
|
33
25
|
extraParams,
|
|
34
26
|
resource
|
|
35
|
-
}: {
|
|
36
|
-
refreshToken: string;
|
|
37
|
-
options: ProviderOptions;
|
|
38
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
39
|
-
extraParams?: Record<string, string> | undefined;
|
|
40
|
-
resource?: (string | string[]) | undefined;
|
|
41
|
-
}): {
|
|
27
|
+
}: RefreshAccessTokenRequestInput): Promise<{
|
|
42
28
|
body: URLSearchParams;
|
|
43
|
-
headers: Record<string,
|
|
44
|
-
}
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
}>;
|
|
45
31
|
declare function refreshAccessToken({
|
|
46
32
|
refreshToken,
|
|
47
33
|
options,
|
|
48
34
|
tokenEndpoint,
|
|
49
35
|
authentication,
|
|
50
|
-
|
|
51
|
-
extraParams
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
options: Partial<ProviderOptions>;
|
|
55
|
-
tokenEndpoint: string;
|
|
56
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
57
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
58
|
-
extraParams?: Record<string, string> | undefined;
|
|
59
|
-
}): Promise<OAuth2Tokens>;
|
|
36
|
+
tokenEndpointAuth,
|
|
37
|
+
extraParams,
|
|
38
|
+
resource
|
|
39
|
+
}: RefreshAccessTokenInput): Promise<OAuth2Tokens>;
|
|
60
40
|
//#endregion
|
|
61
|
-
export {
|
|
41
|
+
export { refreshAccessToken, refreshAccessTokenRequest };
|
|
@@ -1,33 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { parseScopeField } from "./utils.mjs";
|
|
2
|
+
import { fetchRefusingRedirects } from "./reject-redirects.mjs";
|
|
3
|
+
import { applyTokenEndpointAuth } from "./token-endpoint-auth.mjs";
|
|
4
4
|
//#region src/oauth2/refresh-access-token.ts
|
|
5
|
-
|
|
5
|
+
const BLOCKED_REFRESH_TOKEN_PARAMS_SET = new Set([
|
|
6
|
+
"grant_type",
|
|
7
|
+
"refresh_token",
|
|
8
|
+
"__proto__",
|
|
9
|
+
"constructor",
|
|
10
|
+
"prototype"
|
|
11
|
+
]);
|
|
12
|
+
async function refreshAccessTokenRequest({ refreshToken, options, authentication, tokenEndpointAuth, tokenEndpoint, extraParams, resource }) {
|
|
6
13
|
options = typeof options === "function" ? await options() : options;
|
|
7
|
-
|
|
8
|
-
if (!clientAssertion) throw new Error("private_key_jwt authentication requires a clientAssertion configuration");
|
|
9
|
-
const assertionParams = await resolveAssertionParams({
|
|
10
|
-
clientAssertion,
|
|
11
|
-
clientId: Array.isArray(options.clientId) ? options.clientId[0] : options.clientId,
|
|
12
|
-
tokenEndpoint
|
|
13
|
-
});
|
|
14
|
-
extraParams = {
|
|
15
|
-
...extraParams,
|
|
16
|
-
...assertionParams
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
return createRefreshAccessTokenRequest({
|
|
14
|
+
const request = buildRefreshAccessTokenRequest({
|
|
20
15
|
refreshToken,
|
|
21
16
|
options,
|
|
22
|
-
authentication,
|
|
23
17
|
extraParams,
|
|
24
18
|
resource
|
|
25
19
|
});
|
|
20
|
+
await applyTokenEndpointAuth({
|
|
21
|
+
body: request.body,
|
|
22
|
+
headers: request.headers,
|
|
23
|
+
options,
|
|
24
|
+
tokenEndpoint: tokenEndpoint ?? "",
|
|
25
|
+
grantType: "refresh_token",
|
|
26
|
+
tokenEndpointAuth,
|
|
27
|
+
authentication
|
|
28
|
+
});
|
|
29
|
+
return request;
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
function applyRefreshExtraParams(body, extraParams) {
|
|
32
|
+
if (!extraParams) return;
|
|
33
|
+
for (const [key, value] of Object.entries(extraParams)) {
|
|
34
|
+
if (BLOCKED_REFRESH_TOKEN_PARAMS_SET.has(key)) continue;
|
|
35
|
+
body.set(key, value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function buildRefreshAccessTokenRequest({ refreshToken, options, extraParams, resource }) {
|
|
31
39
|
const body = new URLSearchParams();
|
|
32
40
|
const headers = {
|
|
33
41
|
"content-type": "application/x-www-form-urlencoded",
|
|
@@ -35,31 +43,25 @@ function createRefreshAccessTokenRequest({ refreshToken, options, authentication
|
|
|
35
43
|
};
|
|
36
44
|
body.set("grant_type", "refresh_token");
|
|
37
45
|
body.set("refresh_token", refreshToken);
|
|
38
|
-
const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId;
|
|
39
|
-
if (authentication === "basic") if (primaryClientId) headers["authorization"] = "Basic " + base64.encode(`${primaryClientId}:${options.clientSecret ?? ""}`);
|
|
40
|
-
else headers["authorization"] = "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
|
|
41
|
-
else {
|
|
42
|
-
body.set("client_id", primaryClientId);
|
|
43
|
-
if (authentication !== "private_key_jwt" && options.clientSecret) body.set("client_secret", options.clientSecret);
|
|
44
|
-
}
|
|
45
46
|
if (resource) if (typeof resource === "string") body.append("resource", resource);
|
|
46
47
|
else for (const _resource of resource) body.append("resource", _resource);
|
|
47
|
-
if (extraParams)
|
|
48
|
+
if (extraParams) applyRefreshExtraParams(body, extraParams);
|
|
48
49
|
return {
|
|
49
50
|
body,
|
|
50
51
|
headers
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication,
|
|
54
|
+
async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authentication, tokenEndpointAuth, extraParams, resource }) {
|
|
54
55
|
const { body, headers } = await refreshAccessTokenRequest({
|
|
55
56
|
refreshToken,
|
|
56
57
|
options,
|
|
57
58
|
authentication,
|
|
58
|
-
|
|
59
|
+
tokenEndpointAuth,
|
|
59
60
|
tokenEndpoint,
|
|
60
|
-
extraParams
|
|
61
|
+
extraParams,
|
|
62
|
+
resource
|
|
61
63
|
});
|
|
62
|
-
const { data, error } = await
|
|
64
|
+
const { data, error } = await fetchRefusingRedirects(tokenEndpoint, {
|
|
63
65
|
method: "POST",
|
|
64
66
|
body,
|
|
65
67
|
headers
|
|
@@ -69,7 +71,7 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
|
|
|
69
71
|
accessToken: data.access_token,
|
|
70
72
|
refreshToken: data.refresh_token,
|
|
71
73
|
tokenType: data.token_type,
|
|
72
|
-
scopes: data.scope
|
|
74
|
+
scopes: parseScopeField(data.scope),
|
|
73
75
|
idToken: data.id_token
|
|
74
76
|
};
|
|
75
77
|
if (data.expires_in) {
|
|
@@ -83,4 +85,4 @@ async function refreshAccessToken({ refreshToken, options, tokenEndpoint, authen
|
|
|
83
85
|
return tokens;
|
|
84
86
|
}
|
|
85
87
|
//#endregion
|
|
86
|
-
export {
|
|
88
|
+
export { refreshAccessToken, refreshAccessTokenRequest };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BetterAuthError } from "../error/index.mjs";
|
|
2
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
+
//#region src/oauth2/reject-redirects.ts
|
|
4
|
+
const httpRedirectStatuses = new Set([
|
|
5
|
+
301,
|
|
6
|
+
302,
|
|
7
|
+
303,
|
|
8
|
+
307,
|
|
9
|
+
308
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Whether a response from a `redirect: "manual"` fetch is a redirect.
|
|
13
|
+
*
|
|
14
|
+
* Node/undici exposes the real 3xx status. Spec-compliant runtimes (Cloudflare
|
|
15
|
+
* Workers, Deno, browsers) return an opaque-redirect filtered response with
|
|
16
|
+
* status 0 and type `"opaqueredirect"`, so the status alone is not enough.
|
|
17
|
+
*/
|
|
18
|
+
function isRedirectResponse(response) {
|
|
19
|
+
return response.type === "opaqueredirect" || httpRedirectStatuses.has(response.status);
|
|
20
|
+
}
|
|
21
|
+
function redirectRefused(endpoint) {
|
|
22
|
+
return new BetterAuthError(`The OAuth endpoint "${endpoint}" returned an HTTP redirect. Server-side OAuth fetches refuse redirects to prevent SSRF; configure the final endpoint URL.`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fetch option that refuses HTTP redirects portably.
|
|
26
|
+
*
|
|
27
|
+
* Cloudflare Workers (workerd) rejects `redirect: "error"`, so manual mode is
|
|
28
|
+
* used and the resolved response is checked with {@link assertResponseNotRedirect}
|
|
29
|
+
* (or, for betterFetch, with {@link fetchRefusingRedirects}).
|
|
30
|
+
*/
|
|
31
|
+
const noFollowRedirect = { redirect: "manual" };
|
|
32
|
+
/**
|
|
33
|
+
* Throw when a native-`fetch` response (e.g. jose's JWKS loader) resolved to a
|
|
34
|
+
* redirect, so an attacker-influenced endpoint cannot bounce a server-side
|
|
35
|
+
* request to an internal address.
|
|
36
|
+
*/
|
|
37
|
+
function assertResponseNotRedirect(endpoint, response) {
|
|
38
|
+
if (isRedirectResponse(response)) throw redirectRefused(endpoint);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* betterFetch that refuses HTTP redirects on a server-side OAuth fetch.
|
|
42
|
+
*
|
|
43
|
+
* Returns the betterFetch result and throws if the endpoint redirected, on both
|
|
44
|
+
* undici (real 3xx status) and spec-compliant runtimes (opaque redirect, where
|
|
45
|
+
* the error status is 0). The redirect is never followed on any runtime.
|
|
46
|
+
*/
|
|
47
|
+
async function fetchRefusingRedirects(url, options) {
|
|
48
|
+
let redirected = false;
|
|
49
|
+
const onError = options?.onError;
|
|
50
|
+
const result = await betterFetch(url, {
|
|
51
|
+
...options,
|
|
52
|
+
...noFollowRedirect,
|
|
53
|
+
async onError(context) {
|
|
54
|
+
if (isRedirectResponse(context.response)) redirected = true;
|
|
55
|
+
await onError?.(context);
|
|
56
|
+
}
|
|
57
|
+
}).catch((error) => {
|
|
58
|
+
if (redirected) throw redirectRefused(url);
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
if (redirected) throw redirectRefused(url);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { assertResponseNotRedirect, fetchRefusingRedirects, noFollowRedirect };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ClientAssertionGetter } from "./client-assertion.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/oauth2/token-endpoint-auth.d.ts
|
|
4
|
+
type TokenEndpointAuth = {
|
|
5
|
+
method: "none";
|
|
6
|
+
} | {
|
|
7
|
+
method: "client_secret_basic";
|
|
8
|
+
} | {
|
|
9
|
+
method: "client_secret_post";
|
|
10
|
+
} | {
|
|
11
|
+
method: "private_key_jwt";
|
|
12
|
+
getClientAssertion: ClientAssertionGetter;
|
|
13
|
+
};
|
|
14
|
+
type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
|
|
15
|
+
type TokenEndpointSecretAuthentication = "basic" | "post";
|
|
16
|
+
//#endregion
|
|
17
|
+
export { TokenEndpointAuth, TokenEndpointAuthMethod, TokenEndpointSecretAuthentication };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getPrimaryClientId } from "./utils.mjs";
|
|
2
|
+
import { encodeBasicCredentials } from "./basic-credentials.mjs";
|
|
3
|
+
import { resolveClientAssertionParams } from "./client-assertion.mjs";
|
|
4
|
+
//#region src/oauth2/token-endpoint-auth.ts
|
|
5
|
+
function getDefaultTokenEndpointAuth(options, authentication) {
|
|
6
|
+
if (authentication === "basic") return { method: "client_secret_basic" };
|
|
7
|
+
if (options.clientSecret) return { method: "client_secret_post" };
|
|
8
|
+
return { method: "none" };
|
|
9
|
+
}
|
|
10
|
+
function assertNoClientSecret(method, options, body) {
|
|
11
|
+
if (options.clientSecret || body.has("client_secret")) throw new Error(`${method} token endpoint authentication cannot be combined with clientSecret`);
|
|
12
|
+
}
|
|
13
|
+
function setClientId(body, clientId) {
|
|
14
|
+
if (clientId) body.set("client_id", clientId);
|
|
15
|
+
}
|
|
16
|
+
function assertClientSecretConfigured(method, options) {
|
|
17
|
+
if (!options.clientSecret) throw new Error(`${method} token endpoint authentication requires clientSecret`);
|
|
18
|
+
}
|
|
19
|
+
function assertClientIdConfigured(method, clientId) {
|
|
20
|
+
if (!clientId) throw new Error(`${method} token endpoint authentication requires clientId`);
|
|
21
|
+
}
|
|
22
|
+
function setClientSecretPostAuth({ body, options, clientId, requireClientSecret }) {
|
|
23
|
+
if (requireClientSecret) assertClientSecretConfigured("client_secret_post", options);
|
|
24
|
+
if (options.clientSecret) {
|
|
25
|
+
assertClientIdConfigured("client_secret_post", clientId);
|
|
26
|
+
setClientId(body, clientId);
|
|
27
|
+
body.set("client_secret", options.clientSecret);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function setClientSecretBasicAuth({ headers, options, clientId, body }) {
|
|
31
|
+
if (body.has("client_secret")) throw new Error("client_secret_basic token endpoint authentication cannot be combined with client_secret body parameters");
|
|
32
|
+
assertClientSecretConfigured("client_secret_basic", options);
|
|
33
|
+
assertClientIdConfigured("client_secret_basic", clientId);
|
|
34
|
+
headers.authorization = encodeBasicCredentials(clientId, options.clientSecret);
|
|
35
|
+
}
|
|
36
|
+
function assertCompleteManualClientAssertion(body) {
|
|
37
|
+
if (body.has("client_assertion") !== body.has("client_assertion_type")) throw new Error("client_assertion and client_assertion_type must both be provided");
|
|
38
|
+
}
|
|
39
|
+
async function applyTokenEndpointAuth({ body, headers, options, tokenEndpoint, grantType, tokenEndpointAuth, authentication }) {
|
|
40
|
+
assertCompleteManualClientAssertion(body);
|
|
41
|
+
const clientId = getPrimaryClientId(options.clientId);
|
|
42
|
+
if (body.has("client_assertion")) {
|
|
43
|
+
if (tokenEndpointAuth) throw new Error("client_assertion body parameters cannot be combined with tokenEndpointAuth");
|
|
44
|
+
assertNoClientSecret("private_key_jwt", options, body);
|
|
45
|
+
setClientId(body, clientId);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const auth = tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
|
|
49
|
+
if (auth.method === "private_key_jwt") {
|
|
50
|
+
assertNoClientSecret(auth.method, options, body);
|
|
51
|
+
assertClientIdConfigured(auth.method, clientId);
|
|
52
|
+
if (!tokenEndpoint) throw new Error("private_key_jwt token endpoint authentication requires tokenEndpoint");
|
|
53
|
+
const assertionParams = await resolveClientAssertionParams({
|
|
54
|
+
getClientAssertion: auth.getClientAssertion,
|
|
55
|
+
context: {
|
|
56
|
+
clientId,
|
|
57
|
+
tokenEndpoint,
|
|
58
|
+
grantType
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
setClientId(body, clientId);
|
|
62
|
+
for (const [key, value] of Object.entries(assertionParams)) body.set(key, value);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (auth.method === "none") {
|
|
66
|
+
assertNoClientSecret(auth.method, options, body);
|
|
67
|
+
if (grantType === "client_credentials") throw new Error("none token endpoint authentication cannot be used with client_credentials grant");
|
|
68
|
+
assertClientIdConfigured(auth.method, clientId);
|
|
69
|
+
setClientId(body, clientId);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (auth.method === "client_secret_basic") {
|
|
73
|
+
setClientSecretBasicAuth({
|
|
74
|
+
headers,
|
|
75
|
+
options,
|
|
76
|
+
clientId,
|
|
77
|
+
body
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setClientSecretPostAuth({
|
|
82
|
+
body,
|
|
83
|
+
options,
|
|
84
|
+
clientId,
|
|
85
|
+
requireClientSecret: tokenEndpointAuth?.method === "client_secret_post"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { applyTokenEndpointAuth };
|
package/dist/oauth2/utils.d.mts
CHANGED
|
@@ -2,6 +2,28 @@ import { OAuth2Tokens } from "./oauth-provider.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/oauth2/utils.d.ts
|
|
4
4
|
declare function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens;
|
|
5
|
+
/**
|
|
6
|
+
* Fill in `accessTokenExpiresAt` from the provider's configured
|
|
7
|
+
* `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
|
|
8
|
+
* known expiry, `getAccessToken` cannot tell the token is expired and never
|
|
9
|
+
* refreshes it. No-op when the provider already supplied an expiry or no
|
|
10
|
+
* fallback is configured.
|
|
11
|
+
*/
|
|
12
|
+
declare function applyDefaultAccessTokenExpiry(tokens: OAuth2Tokens, accessTokenExpiresIn: number | undefined): OAuth2Tokens;
|
|
13
|
+
/**
|
|
14
|
+
* Compute the union of stored and incoming OAuth scopes, preserving
|
|
15
|
+
* stored insertion order and dropping duplicates.
|
|
16
|
+
*/
|
|
17
|
+
declare function mergeScopes(stored: string | null | undefined, incoming: string[] | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* Return the provider's primary Client ID: the single string, or the entry at
|
|
20
|
+
* array index 0 for the cross-platform form used by ID token audience
|
|
21
|
+
* verification. Index 0 is the designated primary and pairs with
|
|
22
|
+
* `clientSecret` for the authorization code flow; later array entries are
|
|
23
|
+
* only used as additional accepted audiences. Returns `undefined` when the
|
|
24
|
+
* primary value is missing or an empty string.
|
|
25
|
+
*/
|
|
26
|
+
declare function getPrimaryClientId(clientId: unknown): string | undefined;
|
|
5
27
|
declare function generateCodeChallenge(codeVerifier: string): Promise<string>;
|
|
6
28
|
//#endregion
|
|
7
|
-
export { generateCodeChallenge, getOAuth2Tokens };
|
|
29
|
+
export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId, mergeScopes };
|
package/dist/oauth2/utils.mjs
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { base64Url } from "@better-auth/utils/base64";
|
|
2
2
|
//#region src/oauth2/utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parse a provider's `scope` token-response field into a string array.
|
|
5
|
+
*
|
|
6
|
+
* RFC 6749 Section 3.3 defines `scope` as a space-delimited string, but
|
|
7
|
+
* providers vary: some return an already-split array. Accept both forms and
|
|
8
|
+
* drop empty or non-string entries.
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/better-auth/better-auth/issues/9076
|
|
11
|
+
*/
|
|
12
|
+
function parseScopeField(scope) {
|
|
13
|
+
if (Array.isArray(scope)) return scope.map((s) => typeof s === "string" ? s.trim() : "").filter(Boolean);
|
|
14
|
+
if (typeof scope === "string") return scope.trim().split(/\s+/).filter(Boolean);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
3
17
|
function getOAuth2Tokens(data) {
|
|
4
18
|
const getDate = (seconds) => {
|
|
5
19
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -11,15 +25,47 @@ function getOAuth2Tokens(data) {
|
|
|
11
25
|
refreshToken: data.refresh_token,
|
|
12
26
|
accessTokenExpiresAt: data.expires_in ? getDate(data.expires_in) : void 0,
|
|
13
27
|
refreshTokenExpiresAt: data.refresh_token_expires_in ? getDate(data.refresh_token_expires_in) : void 0,
|
|
14
|
-
scopes: data
|
|
28
|
+
scopes: parseScopeField(data.scope),
|
|
15
29
|
idToken: data.id_token,
|
|
16
30
|
raw: data
|
|
17
31
|
};
|
|
18
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Fill in `accessTokenExpiresAt` from the provider's configured
|
|
35
|
+
* `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
|
|
36
|
+
* known expiry, `getAccessToken` cannot tell the token is expired and never
|
|
37
|
+
* refreshes it. No-op when the provider already supplied an expiry or no
|
|
38
|
+
* fallback is configured.
|
|
39
|
+
*/
|
|
40
|
+
function applyDefaultAccessTokenExpiry(tokens, accessTokenExpiresIn) {
|
|
41
|
+
if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) tokens.accessTokenExpiresAt = new Date(Date.now() + accessTokenExpiresIn * 1e3);
|
|
42
|
+
return tokens;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute the union of stored and incoming OAuth scopes, preserving
|
|
46
|
+
* stored insertion order and dropping duplicates.
|
|
47
|
+
*/
|
|
48
|
+
function mergeScopes(stored, incoming) {
|
|
49
|
+
const existing = stored ? stored.split(",").map((scope) => scope.trim()).filter(Boolean) : [];
|
|
50
|
+
const next = (incoming ?? []).map((scope) => scope.trim()).filter(Boolean);
|
|
51
|
+
return [...new Set([...existing, ...next])].join(",");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Return the provider's primary Client ID: the single string, or the entry at
|
|
55
|
+
* array index 0 for the cross-platform form used by ID token audience
|
|
56
|
+
* verification. Index 0 is the designated primary and pairs with
|
|
57
|
+
* `clientSecret` for the authorization code flow; later array entries are
|
|
58
|
+
* only used as additional accepted audiences. Returns `undefined` when the
|
|
59
|
+
* primary value is missing or an empty string.
|
|
60
|
+
*/
|
|
61
|
+
function getPrimaryClientId(clientId) {
|
|
62
|
+
const value = Array.isArray(clientId) ? clientId[0] : clientId;
|
|
63
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
64
|
+
}
|
|
19
65
|
async function generateCodeChallenge(codeVerifier) {
|
|
20
66
|
const data = new TextEncoder().encode(codeVerifier);
|
|
21
67
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
22
68
|
return base64Url.encode(new Uint8Array(hash), { padding: false });
|
|
23
69
|
}
|
|
24
70
|
//#endregion
|
|
25
|
-
export { generateCodeChallenge, getOAuth2Tokens };
|
|
71
|
+
export { applyDefaultAccessTokenExpiry, generateCodeChallenge, getOAuth2Tokens, getPrimaryClientId, mergeScopes, parseScopeField };
|
|
@@ -1,64 +1,41 @@
|
|
|
1
|
-
import { ClientAssertionConfig } from "./client-assertion.mjs";
|
|
2
1
|
import { AwaitableFunction } from "../types/helper.mjs";
|
|
3
2
|
import { OAuth2Tokens, ProviderOptions } from "./oauth-provider.mjs";
|
|
3
|
+
import { TokenEndpointAuth, TokenEndpointSecretAuthentication } from "./token-endpoint-auth.mjs";
|
|
4
4
|
import * as jose from "jose";
|
|
5
5
|
|
|
6
6
|
//#region src/oauth2/validate-authorization-code.d.ts
|
|
7
|
-
|
|
8
|
-
code,
|
|
9
|
-
codeVerifier,
|
|
10
|
-
redirectURI,
|
|
11
|
-
options,
|
|
12
|
-
authentication,
|
|
13
|
-
clientAssertion,
|
|
14
|
-
tokenEndpoint,
|
|
15
|
-
deviceId,
|
|
16
|
-
headers,
|
|
17
|
-
additionalParams,
|
|
18
|
-
resource
|
|
19
|
-
}: {
|
|
7
|
+
interface AuthorizationCodeRequestInput {
|
|
20
8
|
code: string;
|
|
21
9
|
redirectURI: string;
|
|
22
10
|
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
23
11
|
codeVerifier?: string | undefined;
|
|
24
12
|
deviceId?: string | undefined;
|
|
25
|
-
authentication?:
|
|
26
|
-
|
|
13
|
+
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
14
|
+
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
27
15
|
tokenEndpoint?: string | undefined;
|
|
28
16
|
headers?: Record<string, string> | undefined;
|
|
29
17
|
additionalParams?: Record<string, string> | undefined;
|
|
30
18
|
resource?: (string | string[]) | undefined;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
* @deprecated use async'd authorizationCodeRequest instead
|
|
37
|
-
*/
|
|
38
|
-
declare function createAuthorizationCodeRequest({
|
|
19
|
+
}
|
|
20
|
+
interface ValidateAuthorizationCodeInput extends AuthorizationCodeRequestInput {
|
|
21
|
+
tokenEndpoint: string;
|
|
22
|
+
}
|
|
23
|
+
declare function authorizationCodeRequest({
|
|
39
24
|
code,
|
|
40
25
|
codeVerifier,
|
|
41
26
|
redirectURI,
|
|
42
27
|
options,
|
|
43
28
|
authentication,
|
|
29
|
+
tokenEndpointAuth,
|
|
30
|
+
tokenEndpoint,
|
|
44
31
|
deviceId,
|
|
45
32
|
headers,
|
|
46
33
|
additionalParams,
|
|
47
34
|
resource
|
|
48
|
-
}: {
|
|
49
|
-
code: string;
|
|
50
|
-
redirectURI: string;
|
|
51
|
-
options: Partial<ProviderOptions>;
|
|
52
|
-
codeVerifier?: string | undefined;
|
|
53
|
-
deviceId?: string | undefined;
|
|
54
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
55
|
-
headers?: Record<string, string> | undefined;
|
|
56
|
-
additionalParams?: Record<string, string> | undefined;
|
|
57
|
-
resource?: (string | string[]) | undefined;
|
|
58
|
-
}): {
|
|
35
|
+
}: AuthorizationCodeRequestInput): Promise<{
|
|
59
36
|
body: URLSearchParams;
|
|
60
|
-
headers: Record<string,
|
|
61
|
-
}
|
|
37
|
+
headers: Record<string, string>;
|
|
38
|
+
}>;
|
|
62
39
|
declare function validateAuthorizationCode({
|
|
63
40
|
code,
|
|
64
41
|
codeVerifier,
|
|
@@ -66,27 +43,15 @@ declare function validateAuthorizationCode({
|
|
|
66
43
|
options,
|
|
67
44
|
tokenEndpoint,
|
|
68
45
|
authentication,
|
|
69
|
-
|
|
46
|
+
tokenEndpointAuth,
|
|
70
47
|
deviceId,
|
|
71
48
|
headers,
|
|
72
49
|
additionalParams,
|
|
73
50
|
resource
|
|
74
|
-
}:
|
|
75
|
-
code: string;
|
|
76
|
-
redirectURI: string;
|
|
77
|
-
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
78
|
-
codeVerifier?: string | undefined;
|
|
79
|
-
deviceId?: string | undefined;
|
|
80
|
-
tokenEndpoint: string;
|
|
81
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
82
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
83
|
-
headers?: Record<string, string> | undefined;
|
|
84
|
-
additionalParams?: Record<string, string> | undefined;
|
|
85
|
-
resource?: (string | string[]) | undefined;
|
|
86
|
-
}): Promise<OAuth2Tokens>;
|
|
51
|
+
}: ValidateAuthorizationCodeInput): Promise<OAuth2Tokens>;
|
|
87
52
|
declare function validateToken(token: string, jwksEndpoint: string, options?: {
|
|
88
53
|
audience?: string | string[];
|
|
89
54
|
issuer?: string | string[];
|
|
90
55
|
}): Promise<jose.JWTVerifyResult<jose.JWTPayload> & jose.ResolvedKey>;
|
|
91
56
|
//#endregion
|
|
92
|
-
export { authorizationCodeRequest,
|
|
57
|
+
export { authorizationCodeRequest, validateAuthorizationCode, validateToken };
|