@depup/better-auth__core 1.6.23-depup.2
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/LICENSE.md +20 -0
- package/README.md +32 -0
- package/changes.json +14 -0
- package/dist/api/index.d.mts +281 -0
- package/dist/api/index.mjs +92 -0
- package/dist/async_hooks/index.d.mts +6 -0
- package/dist/async_hooks/index.mjs +20 -0
- package/dist/async_hooks/pure.index.d.mts +6 -0
- package/dist/async_hooks/pure.index.mjs +33 -0
- package/dist/context/endpoint-context.d.mts +18 -0
- package/dist/context/endpoint-context.mjs +29 -0
- package/dist/context/global.d.mts +6 -0
- package/dist/context/global.mjs +36 -0
- package/dist/context/index.d.mts +5 -0
- package/dist/context/index.mjs +5 -0
- package/dist/context/request-state.d.mts +27 -0
- package/dist/context/request-state.mjs +47 -0
- package/dist/context/transaction.d.mts +27 -0
- package/dist/context/transaction.mjs +96 -0
- package/dist/db/adapter/factory.d.mts +17 -0
- package/dist/db/adapter/factory.mjs +905 -0
- package/dist/db/adapter/get-default-field-name.d.mts +18 -0
- package/dist/db/adapter/get-default-field-name.mjs +36 -0
- package/dist/db/adapter/get-default-model-name.d.mts +12 -0
- package/dist/db/adapter/get-default-model-name.mjs +30 -0
- package/dist/db/adapter/get-field-attributes.d.mts +26 -0
- package/dist/db/adapter/get-field-attributes.mjs +37 -0
- package/dist/db/adapter/get-field-name.d.mts +18 -0
- package/dist/db/adapter/get-field-name.mjs +31 -0
- package/dist/db/adapter/get-id-field.d.mts +36 -0
- package/dist/db/adapter/get-id-field.mjs +64 -0
- package/dist/db/adapter/get-model-name.d.mts +12 -0
- package/dist/db/adapter/get-model-name.mjs +21 -0
- package/dist/db/adapter/index.d.mts +615 -0
- package/dist/db/adapter/index.mjs +24 -0
- package/dist/db/adapter/types.d.mts +105 -0
- package/dist/db/adapter/utils.d.mts +7 -0
- package/dist/db/adapter/utils.mjs +37 -0
- package/dist/db/get-tables.d.mts +6 -0
- package/dist/db/get-tables.mjs +266 -0
- package/dist/db/index.d.mts +10 -0
- package/dist/db/index.mjs +8 -0
- package/dist/db/plugin.d.mts +12 -0
- package/dist/db/schema/account.d.mts +28 -0
- package/dist/db/schema/account.mjs +17 -0
- package/dist/db/schema/rate-limit.d.mts +18 -0
- package/dist/db/schema/rate-limit.mjs +9 -0
- package/dist/db/schema/session.d.mts +23 -0
- package/dist/db/schema/session.mjs +12 -0
- package/dist/db/schema/shared.d.mts +10 -0
- package/dist/db/schema/shared.mjs +9 -0
- package/dist/db/schema/user.d.mts +22 -0
- package/dist/db/schema/user.mjs +11 -0
- package/dist/db/schema/verification.d.mts +21 -0
- package/dist/db/schema/verification.mjs +10 -0
- package/dist/db/type.d.mts +194 -0
- package/dist/env/color-depth.d.mts +4 -0
- package/dist/env/color-depth.mjs +86 -0
- package/dist/env/env-impl.d.mts +32 -0
- package/dist/env/env-impl.mjs +81 -0
- package/dist/env/index.d.mts +4 -0
- package/dist/env/index.mjs +4 -0
- package/dist/env/logger.d.mts +48 -0
- package/dist/env/logger.mjs +79 -0
- package/dist/error/codes.d.mts +69 -0
- package/dist/error/codes.mjs +55 -0
- package/dist/error/index.d.mts +26 -0
- package/dist/error/index.mjs +27 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.mjs +1 -0
- package/dist/instrumentation/api.mjs +12 -0
- package/dist/instrumentation/attributes.d.mts +11 -0
- package/dist/instrumentation/attributes.mjs +10 -0
- package/dist/instrumentation/index.d.mts +3 -0
- package/dist/instrumentation/index.mjs +3 -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.d.mts +13 -0
- package/dist/instrumentation/tracer.mjs +53 -0
- package/dist/oauth2/client-credentials-token.d.mts +56 -0
- package/dist/oauth2/client-credentials-token.mjs +64 -0
- package/dist/oauth2/create-authorization-url.d.mts +44 -0
- package/dist/oauth2/create-authorization-url.mjs +41 -0
- package/dist/oauth2/index.d.mts +8 -0
- package/dist/oauth2/index.mjs +7 -0
- package/dist/oauth2/oauth-provider.d.mts +192 -0
- package/dist/oauth2/refresh-access-token.d.mts +54 -0
- package/dist/oauth2/refresh-access-token.mjs +73 -0
- package/dist/oauth2/reject-redirects.mjs +60 -0
- package/dist/oauth2/utils.d.mts +24 -0
- package/dist/oauth2/utils.mjs +48 -0
- package/dist/oauth2/validate-authorization-code.d.mts +85 -0
- package/dist/oauth2/validate-authorization-code.mjs +86 -0
- package/dist/oauth2/verify.d.mts +65 -0
- package/dist/oauth2/verify.mjs +202 -0
- package/dist/social-providers/apple.d.mts +133 -0
- package/dist/social-providers/apple.mjs +117 -0
- package/dist/social-providers/atlassian.d.mts +70 -0
- package/dist/social-providers/atlassian.mjs +80 -0
- package/dist/social-providers/cognito.d.mts +85 -0
- package/dist/social-providers/cognito.mjs +162 -0
- package/dist/social-providers/discord.d.mts +124 -0
- package/dist/social-providers/discord.mjs +62 -0
- package/dist/social-providers/dropbox.d.mts +69 -0
- package/dist/social-providers/dropbox.mjs +72 -0
- package/dist/social-providers/facebook.d.mts +79 -0
- package/dist/social-providers/facebook.mjs +157 -0
- package/dist/social-providers/figma.d.mts +61 -0
- package/dist/social-providers/figma.mjs +83 -0
- package/dist/social-providers/github.d.mts +102 -0
- package/dist/social-providers/github.mjs +92 -0
- package/dist/social-providers/gitlab.d.mts +123 -0
- package/dist/social-providers/gitlab.mjs +79 -0
- package/dist/social-providers/google.d.mts +123 -0
- package/dist/social-providers/google.mjs +137 -0
- package/dist/social-providers/huggingface.d.mts +83 -0
- package/dist/social-providers/huggingface.mjs +73 -0
- package/dist/social-providers/index.d.mts +1834 -0
- package/dist/social-providers/index.mjs +78 -0
- package/dist/social-providers/kakao.d.mts +161 -0
- package/dist/social-providers/kakao.mjs +70 -0
- package/dist/social-providers/kick.d.mts +73 -0
- package/dist/social-providers/kick.mjs +68 -0
- package/dist/social-providers/line.d.mts +105 -0
- package/dist/social-providers/line.mjs +110 -0
- package/dist/social-providers/linear.d.mts +68 -0
- package/dist/social-providers/linear.mjs +85 -0
- package/dist/social-providers/linkedin.d.mts +67 -0
- package/dist/social-providers/linkedin.mjs +73 -0
- package/dist/social-providers/microsoft-entra-id.d.mts +174 -0
- package/dist/social-providers/microsoft-entra-id.mjs +152 -0
- package/dist/social-providers/naver.d.mts +92 -0
- package/dist/social-providers/naver.mjs +65 -0
- package/dist/social-providers/notion.d.mts +64 -0
- package/dist/social-providers/notion.mjs +72 -0
- package/dist/social-providers/paybin.d.mts +71 -0
- package/dist/social-providers/paybin.mjs +81 -0
- package/dist/social-providers/paypal.d.mts +131 -0
- package/dist/social-providers/paypal.mjs +188 -0
- package/dist/social-providers/polar.d.mts +74 -0
- package/dist/social-providers/polar.mjs +71 -0
- package/dist/social-providers/railway.d.mts +65 -0
- package/dist/social-providers/railway.mjs +74 -0
- package/dist/social-providers/reddit.d.mts +62 -0
- package/dist/social-providers/reddit.mjs +81 -0
- package/dist/social-providers/roblox.d.mts +70 -0
- package/dist/social-providers/roblox.mjs +57 -0
- package/dist/social-providers/salesforce.d.mts +79 -0
- package/dist/social-providers/salesforce.mjs +87 -0
- package/dist/social-providers/slack.d.mts +83 -0
- package/dist/social-providers/slack.mjs +66 -0
- package/dist/social-providers/spotify.d.mts +63 -0
- package/dist/social-providers/spotify.mjs +69 -0
- package/dist/social-providers/tiktok.d.mts +168 -0
- package/dist/social-providers/tiktok.mjs +60 -0
- package/dist/social-providers/twitch.d.mts +79 -0
- package/dist/social-providers/twitch.mjs +75 -0
- package/dist/social-providers/twitter.d.mts +126 -0
- package/dist/social-providers/twitter.mjs +85 -0
- package/dist/social-providers/vercel.d.mts +62 -0
- package/dist/social-providers/vercel.mjs +58 -0
- package/dist/social-providers/vk.d.mts +70 -0
- package/dist/social-providers/vk.mjs +81 -0
- package/dist/social-providers/wechat.d.mts +113 -0
- package/dist/social-providers/wechat.mjs +81 -0
- package/dist/social-providers/zoom.d.mts +163 -0
- package/dist/social-providers/zoom.mjs +69 -0
- package/dist/types/context.d.mts +319 -0
- package/dist/types/cookie.d.mts +15 -0
- package/dist/types/helper.d.mts +10 -0
- package/dist/types/index.d.mts +9 -0
- package/dist/types/init-options.d.mts +1430 -0
- package/dist/types/plugin-client.d.mts +123 -0
- package/dist/types/plugin.d.mts +124 -0
- package/dist/types/secret.d.mts +11 -0
- package/dist/utils/async.d.mts +22 -0
- package/dist/utils/async.mjs +32 -0
- package/dist/utils/db.d.mts +9 -0
- package/dist/utils/db.mjs +15 -0
- package/dist/utils/deprecate.d.mts +9 -0
- package/dist/utils/deprecate.mjs +16 -0
- package/dist/utils/error-codes.d.mts +13 -0
- package/dist/utils/error-codes.mjs +10 -0
- package/dist/utils/fetch-metadata.d.mts +4 -0
- package/dist/utils/fetch-metadata.mjs +6 -0
- package/dist/utils/host.d.mts +147 -0
- package/dist/utils/host.mjs +295 -0
- package/dist/utils/id.d.mts +4 -0
- package/dist/utils/id.mjs +7 -0
- package/dist/utils/ip.d.mts +77 -0
- package/dist/utils/ip.mjs +230 -0
- package/dist/utils/is-api-error.d.mts +6 -0
- package/dist/utils/is-api-error.mjs +8 -0
- package/dist/utils/json.d.mts +4 -0
- package/dist/utils/json.mjs +41 -0
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +8 -0
- package/dist/utils/string.mjs +25 -0
- package/dist/utils/url.d.mts +37 -0
- package/dist/utils/url.mjs +61 -0
- package/package.json +216 -0
- package/src/api/index.ts +185 -0
- package/src/async_hooks/index.ts +40 -0
- package/src/async_hooks/pure.index.ts +46 -0
- package/src/context/endpoint-context.ts +50 -0
- package/src/context/global.ts +57 -0
- package/src/context/index.ts +23 -0
- package/src/context/request-state.ts +91 -0
- package/src/context/transaction.ts +169 -0
- package/src/db/adapter/factory.ts +1740 -0
- package/src/db/adapter/get-default-field-name.ts +59 -0
- package/src/db/adapter/get-default-model-name.ts +51 -0
- package/src/db/adapter/get-field-attributes.ts +62 -0
- package/src/db/adapter/get-field-name.ts +43 -0
- package/src/db/adapter/get-id-field.ts +150 -0
- package/src/db/adapter/get-model-name.ts +36 -0
- package/src/db/adapter/index.ts +656 -0
- package/src/db/adapter/types.ts +134 -0
- package/src/db/adapter/utils.ts +61 -0
- package/src/db/get-tables.ts +304 -0
- package/src/db/index.ts +43 -0
- package/src/db/plugin.ts +11 -0
- package/src/db/schema/account.ts +47 -0
- package/src/db/schema/rate-limit.ts +36 -0
- package/src/db/schema/session.ts +29 -0
- package/src/db/schema/shared.ts +7 -0
- package/src/db/schema/user.ts +31 -0
- package/src/db/schema/verification.ts +28 -0
- package/src/db/type.ts +360 -0
- package/src/env/color-depth.ts +172 -0
- package/src/env/env-impl.ts +123 -0
- package/src/env/index.ts +23 -0
- package/src/env/logger.ts +145 -0
- package/src/error/codes.ts +72 -0
- package/src/error/index.ts +44 -0
- package/src/index.ts +1 -0
- package/src/instrumentation/api.ts +17 -0
- package/src/instrumentation/attributes.ts +22 -0
- package/src/instrumentation/index.ts +2 -0
- package/src/instrumentation/noop.ts +74 -0
- package/src/instrumentation/pure.index.ts +31 -0
- package/src/instrumentation/tracer.ts +95 -0
- package/src/oauth2/client-credentials-token.ts +126 -0
- package/src/oauth2/create-authorization-url.ts +89 -0
- package/src/oauth2/index.ts +34 -0
- package/src/oauth2/oauth-provider.ts +222 -0
- package/src/oauth2/refresh-access-token.ts +157 -0
- package/src/oauth2/reject-redirects.ts +70 -0
- package/src/oauth2/utils.ts +70 -0
- package/src/oauth2/validate-authorization-code.ts +190 -0
- package/src/oauth2/verify.ts +408 -0
- package/src/social-providers/apple.ts +249 -0
- package/src/social-providers/atlassian.ts +133 -0
- package/src/social-providers/cognito.ts +281 -0
- package/src/social-providers/discord.ts +170 -0
- package/src/social-providers/dropbox.ts +112 -0
- package/src/social-providers/facebook.ts +288 -0
- package/src/social-providers/figma.ts +118 -0
- package/src/social-providers/github.ts +184 -0
- package/src/social-providers/gitlab.ts +155 -0
- package/src/social-providers/google.ts +263 -0
- package/src/social-providers/huggingface.ts +119 -0
- package/src/social-providers/index.ts +132 -0
- package/src/social-providers/kakao.ts +179 -0
- package/src/social-providers/kick.ts +109 -0
- package/src/social-providers/line.ts +169 -0
- package/src/social-providers/linear.ts +121 -0
- package/src/social-providers/linkedin.ts +110 -0
- package/src/social-providers/microsoft-entra-id.ts +391 -0
- package/src/social-providers/naver.ts +113 -0
- package/src/social-providers/notion.ts +108 -0
- package/src/social-providers/paybin.ts +118 -0
- package/src/social-providers/paypal.ts +371 -0
- package/src/social-providers/polar.ts +111 -0
- package/src/social-providers/railway.ts +100 -0
- package/src/social-providers/reddit.ts +126 -0
- package/src/social-providers/roblox.ts +112 -0
- package/src/social-providers/salesforce.ts +159 -0
- package/src/social-providers/slack.ts +112 -0
- package/src/social-providers/spotify.ts +94 -0
- package/src/social-providers/tiktok.ts +211 -0
- package/src/social-providers/twitch.ts +112 -0
- package/src/social-providers/twitter.ts +199 -0
- package/src/social-providers/vercel.ts +87 -0
- package/src/social-providers/vk.ts +125 -0
- package/src/social-providers/wechat.ts +220 -0
- package/src/social-providers/zoom.ts +230 -0
- package/src/types/context.ts +460 -0
- package/src/types/cookie.ts +10 -0
- package/src/types/helper.ts +27 -0
- package/src/types/index.ts +40 -0
- package/src/types/init-options.ts +1679 -0
- package/src/types/plugin-client.ts +143 -0
- package/src/types/plugin.ts +163 -0
- package/src/types/secret.ts +8 -0
- package/src/utils/async.ts +53 -0
- package/src/utils/db.ts +20 -0
- package/src/utils/deprecate.ts +21 -0
- package/src/utils/error-codes.ts +68 -0
- package/src/utils/fetch-metadata.ts +3 -0
- package/src/utils/host.ts +416 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/ip.ts +395 -0
- package/src/utils/is-api-error.ts +10 -0
- package/src/utils/json.ts +56 -0
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +40 -0
- package/src/utils/url.ts +77 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
2
|
+
import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
|
|
3
|
+
import type { AwaitableFunction } from "../types";
|
|
4
|
+
import type { ProviderOptions } from "./index";
|
|
5
|
+
import { getOAuth2Tokens } from "./index";
|
|
6
|
+
import {
|
|
7
|
+
assertResponseNotRedirect,
|
|
8
|
+
fetchRefusingRedirects,
|
|
9
|
+
NO_FOLLOW_REDIRECT,
|
|
10
|
+
} from "./reject-redirects";
|
|
11
|
+
|
|
12
|
+
export async function authorizationCodeRequest({
|
|
13
|
+
code,
|
|
14
|
+
codeVerifier,
|
|
15
|
+
redirectURI,
|
|
16
|
+
options,
|
|
17
|
+
authentication,
|
|
18
|
+
deviceId,
|
|
19
|
+
headers,
|
|
20
|
+
additionalParams = {},
|
|
21
|
+
resource,
|
|
22
|
+
}: {
|
|
23
|
+
code: string;
|
|
24
|
+
redirectURI: string;
|
|
25
|
+
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
26
|
+
codeVerifier?: string | undefined;
|
|
27
|
+
deviceId?: string | undefined;
|
|
28
|
+
authentication?: ("basic" | "post") | undefined;
|
|
29
|
+
headers?: Record<string, string> | undefined;
|
|
30
|
+
additionalParams?: Record<string, string> | undefined;
|
|
31
|
+
resource?: (string | string[]) | undefined;
|
|
32
|
+
}) {
|
|
33
|
+
options = typeof options === "function" ? await options() : options;
|
|
34
|
+
return createAuthorizationCodeRequest({
|
|
35
|
+
code,
|
|
36
|
+
codeVerifier,
|
|
37
|
+
redirectURI,
|
|
38
|
+
options,
|
|
39
|
+
authentication,
|
|
40
|
+
deviceId,
|
|
41
|
+
headers,
|
|
42
|
+
additionalParams,
|
|
43
|
+
resource,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated use async'd authorizationCodeRequest instead
|
|
49
|
+
*/
|
|
50
|
+
export function createAuthorizationCodeRequest({
|
|
51
|
+
code,
|
|
52
|
+
codeVerifier,
|
|
53
|
+
redirectURI,
|
|
54
|
+
options,
|
|
55
|
+
authentication,
|
|
56
|
+
deviceId,
|
|
57
|
+
headers,
|
|
58
|
+
additionalParams = {},
|
|
59
|
+
resource,
|
|
60
|
+
}: {
|
|
61
|
+
code: string;
|
|
62
|
+
redirectURI: string;
|
|
63
|
+
options: Partial<ProviderOptions>;
|
|
64
|
+
codeVerifier?: string | undefined;
|
|
65
|
+
deviceId?: string | undefined;
|
|
66
|
+
authentication?: ("basic" | "post") | undefined;
|
|
67
|
+
headers?: Record<string, string> | undefined;
|
|
68
|
+
additionalParams?: Record<string, string> | undefined;
|
|
69
|
+
resource?: (string | string[]) | undefined;
|
|
70
|
+
}) {
|
|
71
|
+
const body = new URLSearchParams();
|
|
72
|
+
const requestHeaders: Record<string, any> = {
|
|
73
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
74
|
+
accept: "application/json",
|
|
75
|
+
...headers,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
body.set("grant_type", "authorization_code");
|
|
79
|
+
body.set("code", code);
|
|
80
|
+
codeVerifier && body.set("code_verifier", codeVerifier);
|
|
81
|
+
options.clientKey && body.set("client_key", options.clientKey);
|
|
82
|
+
deviceId && body.set("device_id", deviceId);
|
|
83
|
+
body.set("redirect_uri", options.redirectURI || redirectURI);
|
|
84
|
+
if (resource) {
|
|
85
|
+
if (typeof resource === "string") {
|
|
86
|
+
body.append("resource", resource);
|
|
87
|
+
} else {
|
|
88
|
+
for (const _resource of resource) {
|
|
89
|
+
body.append("resource", _resource);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Use standard Base64 encoding for HTTP Basic Auth (OAuth2 spec, RFC 7617)
|
|
94
|
+
// Fixes compatibility with providers like Notion, Twitter, etc.
|
|
95
|
+
if (authentication === "basic") {
|
|
96
|
+
const primaryClientId = Array.isArray(options.clientId)
|
|
97
|
+
? options.clientId[0]
|
|
98
|
+
: options.clientId;
|
|
99
|
+
const encodedCredentials = base64.encode(
|
|
100
|
+
`${primaryClientId}:${options.clientSecret ?? ""}`,
|
|
101
|
+
);
|
|
102
|
+
requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
|
|
103
|
+
} else {
|
|
104
|
+
const primaryClientId = Array.isArray(options.clientId)
|
|
105
|
+
? options.clientId[0]
|
|
106
|
+
: options.clientId;
|
|
107
|
+
body.set("client_id", primaryClientId);
|
|
108
|
+
if (options.clientSecret) {
|
|
109
|
+
body.set("client_secret", options.clientSecret);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const [key, value] of Object.entries(additionalParams)) {
|
|
114
|
+
if (!body.has(key)) body.append(key, value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
body,
|
|
119
|
+
headers: requestHeaders,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function validateAuthorizationCode({
|
|
124
|
+
code,
|
|
125
|
+
codeVerifier,
|
|
126
|
+
redirectURI,
|
|
127
|
+
options,
|
|
128
|
+
tokenEndpoint,
|
|
129
|
+
authentication,
|
|
130
|
+
deviceId,
|
|
131
|
+
headers,
|
|
132
|
+
additionalParams = {},
|
|
133
|
+
resource,
|
|
134
|
+
}: {
|
|
135
|
+
code: string;
|
|
136
|
+
redirectURI: string;
|
|
137
|
+
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
138
|
+
codeVerifier?: string | undefined;
|
|
139
|
+
deviceId?: string | undefined;
|
|
140
|
+
tokenEndpoint: string;
|
|
141
|
+
authentication?: ("basic" | "post") | undefined;
|
|
142
|
+
headers?: Record<string, string> | undefined;
|
|
143
|
+
additionalParams?: Record<string, string> | undefined;
|
|
144
|
+
resource?: (string | string[]) | undefined;
|
|
145
|
+
}) {
|
|
146
|
+
const { body, headers: requestHeaders } = await authorizationCodeRequest({
|
|
147
|
+
code,
|
|
148
|
+
codeVerifier,
|
|
149
|
+
redirectURI,
|
|
150
|
+
options,
|
|
151
|
+
authentication,
|
|
152
|
+
deviceId,
|
|
153
|
+
headers,
|
|
154
|
+
additionalParams,
|
|
155
|
+
resource,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { data, error } = await fetchRefusingRedirects<object>(tokenEndpoint, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: body,
|
|
161
|
+
headers: requestHeaders,
|
|
162
|
+
});
|
|
163
|
+
if (error) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
const tokens = getOAuth2Tokens(data);
|
|
167
|
+
return tokens;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function validateToken(
|
|
171
|
+
token: string,
|
|
172
|
+
jwksEndpoint: string,
|
|
173
|
+
options?: {
|
|
174
|
+
audience?: string | string[];
|
|
175
|
+
issuer?: string | string[];
|
|
176
|
+
},
|
|
177
|
+
) {
|
|
178
|
+
const jwks = createRemoteJWKSet(new URL(jwksEndpoint), {
|
|
179
|
+
[customFetch]: async (url, init) => {
|
|
180
|
+
const response = await fetch(url, { ...init, ...NO_FOLLOW_REDIRECT });
|
|
181
|
+
assertResponseNotRedirect(String(url), response);
|
|
182
|
+
return response;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const verified = await jwtVerify(token, jwks, {
|
|
186
|
+
audience: options?.audience,
|
|
187
|
+
issuer: options?.issuer,
|
|
188
|
+
});
|
|
189
|
+
return verified;
|
|
190
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { APIError } from "better-call";
|
|
2
|
+
import type {
|
|
3
|
+
JSONWebKeySet,
|
|
4
|
+
JWTPayload,
|
|
5
|
+
JWTVerifyOptions,
|
|
6
|
+
JWTVerifyResult,
|
|
7
|
+
ProtectedHeaderParameters,
|
|
8
|
+
} from "jose";
|
|
9
|
+
import {
|
|
10
|
+
createLocalJWKSet,
|
|
11
|
+
decodeProtectedHeader,
|
|
12
|
+
errors as joseErrors,
|
|
13
|
+
jwtVerify,
|
|
14
|
+
UnsecuredJWT,
|
|
15
|
+
} from "jose";
|
|
16
|
+
import { logger } from "../env";
|
|
17
|
+
import { fetchRefusingRedirects } from "./reject-redirects";
|
|
18
|
+
|
|
19
|
+
const joseInfrastructureErrorCodes = new Set([
|
|
20
|
+
joseErrors.JWKSTimeout.code,
|
|
21
|
+
joseErrors.JWKSInvalid.code,
|
|
22
|
+
joseErrors.JWKSMultipleMatchingKeys.code,
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function isJoseInfrastructureError(error: joseErrors.JOSEError) {
|
|
26
|
+
return joseInfrastructureErrorCodes.has(error.code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface JwksCacheEntry {
|
|
30
|
+
jwks: JSONWebKeySet;
|
|
31
|
+
fetchedAt: number;
|
|
32
|
+
noKidRefetchedAt?: number | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type JwksFetchOptions = {
|
|
36
|
+
/** Jwks url or promise of a Jwks */
|
|
37
|
+
jwksFetch: string | (() => Promise<JSONWebKeySet | undefined>);
|
|
38
|
+
/**
|
|
39
|
+
* Stable object to cache the result of a function `jwksFetch` under,
|
|
40
|
+
* with the same TTL and kid-miss refetch rules as string sources.
|
|
41
|
+
* Without it, a function source is fetched on every verification.
|
|
42
|
+
*/
|
|
43
|
+
jwksCacheKey?: object;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ResolvedJwks = {
|
|
47
|
+
jwks: JSONWebKeySet;
|
|
48
|
+
fromCache: boolean;
|
|
49
|
+
kid: string | undefined;
|
|
50
|
+
noKidRefetchedAt?: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export const jwksCache = new Map<string, JwksCacheEntry>();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cache for function jwks sources, keyed by a caller-provided stable object.
|
|
60
|
+
* Entries are released with their key, so per-request keys cannot accumulate.
|
|
61
|
+
*/
|
|
62
|
+
const functionJwksCache = new WeakMap<object, JwksCacheEntry>();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How long a cached JWKS is trusted before it is refetched
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
70
|
+
const JWKS_NO_KID_REFETCH_COOLDOWN_MS = 30 * 1000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the cached key set when it is within the TTL. When the token carries
|
|
74
|
+
* `kid`, the cached set must contain that key id; without `kid`, key selection
|
|
75
|
+
* is deferred to JOSE because RFC 7515 makes the header parameter optional.
|
|
76
|
+
*/
|
|
77
|
+
function getFreshJwksWithKid(
|
|
78
|
+
cached: JwksCacheEntry | undefined,
|
|
79
|
+
kid: string | undefined,
|
|
80
|
+
): JSONWebKeySet | undefined {
|
|
81
|
+
if (!cached) return undefined;
|
|
82
|
+
if (Date.now() - cached.fetchedAt >= JWKS_CACHE_TTL_MS) return undefined;
|
|
83
|
+
if (kid && !cached.jwks.keys.some((jwk) => jwk.kid === kid)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return cached.jwks;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shouldRefetchCachedJwksWithoutKid(
|
|
90
|
+
error: unknown,
|
|
91
|
+
resolved: ResolvedJwks,
|
|
92
|
+
) {
|
|
93
|
+
const isRetryableNoKidFailure =
|
|
94
|
+
resolved.fromCache &&
|
|
95
|
+
!resolved.kid &&
|
|
96
|
+
(error instanceof joseErrors.JWKSNoMatchingKey ||
|
|
97
|
+
error instanceof joseErrors.JWSSignatureVerificationFailed);
|
|
98
|
+
if (!isRetryableNoKidFailure) return false;
|
|
99
|
+
if (!resolved.noKidRefetchedAt) return true;
|
|
100
|
+
return (
|
|
101
|
+
Date.now() - resolved.noKidRefetchedAt >= JWKS_NO_KID_REFETCH_COOLDOWN_MS
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function fetchJwks(
|
|
106
|
+
jwksFetch: JwksFetchOptions["jwksFetch"],
|
|
107
|
+
): Promise<JSONWebKeySet> {
|
|
108
|
+
const jwks =
|
|
109
|
+
typeof jwksFetch === "string"
|
|
110
|
+
? await fetchRefusingRedirects<JSONWebKeySet>(jwksFetch, {
|
|
111
|
+
headers: {
|
|
112
|
+
Accept: "application/json",
|
|
113
|
+
},
|
|
114
|
+
}).then(async (res) => {
|
|
115
|
+
if (res.error)
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Jwks failed: ${res.error.message ?? res.error.statusText}`,
|
|
118
|
+
);
|
|
119
|
+
return res.data;
|
|
120
|
+
})
|
|
121
|
+
: await jwksFetch();
|
|
122
|
+
if (!jwks) throw new Error("No jwks found");
|
|
123
|
+
return jwks;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface VerifyAccessTokenRemote {
|
|
127
|
+
/** Full url of the introspect endpoint. Should end with `/oauth2/introspect` */
|
|
128
|
+
introspectUrl: string;
|
|
129
|
+
/** Client Secret */
|
|
130
|
+
clientId: string;
|
|
131
|
+
/** Client Secret */
|
|
132
|
+
clientSecret: string;
|
|
133
|
+
/**
|
|
134
|
+
* Forces remote verification of a token.
|
|
135
|
+
* This ensures attached session (if applicable)
|
|
136
|
+
* is also still active.
|
|
137
|
+
*/
|
|
138
|
+
force?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Accept introspection responses that omit the `aud` claim even when a
|
|
141
|
+
* required `audience` is configured in `verifyOptions`.
|
|
142
|
+
*
|
|
143
|
+
* By default verification fails closed: if you configure an `audience` and
|
|
144
|
+
* the introspection response has no `aud` (or a mismatching one), the token
|
|
145
|
+
* is rejected. Some authorization servers legitimately omit `aud` from
|
|
146
|
+
* introspection responses (it is OPTIONAL per RFC 7662 §2.2); only enable
|
|
147
|
+
* this if you trust the issuer to bind the token to this resource through
|
|
148
|
+
* another mechanism, as it skips the audience check in that case.
|
|
149
|
+
*
|
|
150
|
+
* @default false
|
|
151
|
+
*/
|
|
152
|
+
allowMissingAudience?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Performs local verification of an access token for your APIs.
|
|
157
|
+
*
|
|
158
|
+
* Can also be configured for remote verification.
|
|
159
|
+
*/
|
|
160
|
+
export async function verifyJwsAccessToken(
|
|
161
|
+
token: string,
|
|
162
|
+
opts: JwksFetchOptions & {
|
|
163
|
+
/** Verify options */
|
|
164
|
+
verifyOptions: JWTVerifyOptions &
|
|
165
|
+
Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
|
|
166
|
+
},
|
|
167
|
+
) {
|
|
168
|
+
try {
|
|
169
|
+
const resolved = await getJwksForVerification(token, opts);
|
|
170
|
+
let jwt: JWTVerifyResult<JWTPayload>;
|
|
171
|
+
try {
|
|
172
|
+
jwt = await jwtVerify<JWTPayload>(
|
|
173
|
+
token,
|
|
174
|
+
createLocalJWKSet(resolved.jwks),
|
|
175
|
+
opts.verifyOptions,
|
|
176
|
+
);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (shouldRefetchCachedJwksWithoutKid(error, resolved)) {
|
|
179
|
+
const refreshed = await getJwksForVerification(token, {
|
|
180
|
+
...opts,
|
|
181
|
+
forceRefresh: true,
|
|
182
|
+
});
|
|
183
|
+
jwt = await jwtVerify<JWTPayload>(
|
|
184
|
+
token,
|
|
185
|
+
createLocalJWKSet(refreshed.jwks),
|
|
186
|
+
opts.verifyOptions,
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Return the JWT payload in introspection format
|
|
193
|
+
// https://datatracker.ietf.org/doc/html/rfc7662#section-2.2
|
|
194
|
+
if (jwt.payload.azp) {
|
|
195
|
+
jwt.payload.client_id = jwt.payload.azp;
|
|
196
|
+
}
|
|
197
|
+
return jwt.payload;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error instanceof Error) throw error;
|
|
200
|
+
throw new Error(error as unknown as string);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function getJwks(token: string, opts: JwksFetchOptions) {
|
|
205
|
+
return (await getJwksForVerification(token, opts)).jwks;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function getJwksForVerification(
|
|
209
|
+
token: string,
|
|
210
|
+
opts: JwksFetchOptions & { forceRefresh?: boolean },
|
|
211
|
+
) {
|
|
212
|
+
// Attempt to decode the token and find a matching kid in jwks
|
|
213
|
+
let jwtHeaders: ProtectedHeaderParameters | undefined;
|
|
214
|
+
try {
|
|
215
|
+
jwtHeaders = decodeProtectedHeader(token);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof Error) throw error;
|
|
218
|
+
throw new Error(error as unknown as string);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const kid = jwtHeaders.kid;
|
|
222
|
+
|
|
223
|
+
// Function sources have no usable identity of their own (callers pass
|
|
224
|
+
// fresh closures per request), so they are cached only under a stable
|
|
225
|
+
// caller-provided key object.
|
|
226
|
+
if (typeof opts.jwksFetch !== "string") {
|
|
227
|
+
const cacheKey = opts.jwksCacheKey;
|
|
228
|
+
if (!cacheKey) {
|
|
229
|
+
const jwks = await opts.jwksFetch();
|
|
230
|
+
if (!jwks) throw new Error("No jwks found");
|
|
231
|
+
return { jwks, fromCache: false, kid };
|
|
232
|
+
}
|
|
233
|
+
const cached = functionJwksCache.get(cacheKey);
|
|
234
|
+
const cachedJwks = opts.forceRefresh
|
|
235
|
+
? undefined
|
|
236
|
+
: getFreshJwksWithKid(cached, kid);
|
|
237
|
+
if (cachedJwks) {
|
|
238
|
+
return {
|
|
239
|
+
jwks: cachedJwks,
|
|
240
|
+
fromCache: true,
|
|
241
|
+
kid,
|
|
242
|
+
noKidRefetchedAt: cached?.noKidRefetchedAt,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const jwks = await opts.jwksFetch();
|
|
246
|
+
if (!jwks) throw new Error("No jwks found");
|
|
247
|
+
const fetchedAt = Date.now();
|
|
248
|
+
functionJwksCache.set(cacheKey, {
|
|
249
|
+
jwks,
|
|
250
|
+
fetchedAt,
|
|
251
|
+
...(opts.forceRefresh && !kid ? { noKidRefetchedAt: fetchedAt } : {}),
|
|
252
|
+
});
|
|
253
|
+
return { jwks, fromCache: false, kid };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// The cache is scoped to `cacheKey`, so a token is only ever matched
|
|
257
|
+
// against the key set published by its own source.
|
|
258
|
+
const cacheKey = opts.jwksFetch;
|
|
259
|
+
const cached = jwksCache.get(cacheKey);
|
|
260
|
+
const cachedJwks = opts.forceRefresh
|
|
261
|
+
? undefined
|
|
262
|
+
: getFreshJwksWithKid(cached, kid);
|
|
263
|
+
if (!cachedJwks) {
|
|
264
|
+
const jwks = await fetchJwks(opts.jwksFetch);
|
|
265
|
+
const fetchedAt = Date.now();
|
|
266
|
+
jwksCache.set(cacheKey, {
|
|
267
|
+
jwks,
|
|
268
|
+
fetchedAt,
|
|
269
|
+
...(opts.forceRefresh && !kid ? { noKidRefetchedAt: fetchedAt } : {}),
|
|
270
|
+
});
|
|
271
|
+
return { jwks, fromCache: false, kid };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
jwks: cachedJwks,
|
|
276
|
+
fromCache: true,
|
|
277
|
+
kid,
|
|
278
|
+
noKidRefetchedAt: cached?.noKidRefetchedAt,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Performs local verification of an access token for your API.
|
|
284
|
+
*
|
|
285
|
+
* Can also be configured for remote verification.
|
|
286
|
+
*/
|
|
287
|
+
export async function verifyAccessToken(
|
|
288
|
+
token: string,
|
|
289
|
+
opts: {
|
|
290
|
+
/** Verify options */
|
|
291
|
+
verifyOptions: JWTVerifyOptions &
|
|
292
|
+
Required<Pick<JWTVerifyOptions, "audience" | "issuer">>;
|
|
293
|
+
/** Scopes to additionally verify. Token must include all but not exact. */
|
|
294
|
+
scopes?: string[];
|
|
295
|
+
/** Required to verify access token locally */
|
|
296
|
+
jwksUrl?: string;
|
|
297
|
+
/** If provided, can verify a token remotely */
|
|
298
|
+
remoteVerify?: VerifyAccessTokenRemote;
|
|
299
|
+
},
|
|
300
|
+
) {
|
|
301
|
+
let payload: JWTPayload | undefined;
|
|
302
|
+
// Locally verify
|
|
303
|
+
if (opts.jwksUrl && !opts?.remoteVerify?.force) {
|
|
304
|
+
try {
|
|
305
|
+
payload = await verifyJwsAccessToken(token, {
|
|
306
|
+
jwksFetch: opts.jwksUrl,
|
|
307
|
+
verifyOptions: opts.verifyOptions,
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof Error) {
|
|
311
|
+
if (error.name === "TypeError" || error.name === "JWSInvalid") {
|
|
312
|
+
// likely an opaque token (continue)
|
|
313
|
+
} else if (error instanceof joseErrors.JWTExpired) {
|
|
314
|
+
throw new APIError("UNAUTHORIZED", {
|
|
315
|
+
message: "token expired",
|
|
316
|
+
});
|
|
317
|
+
} else if (error instanceof joseErrors.JOSEError) {
|
|
318
|
+
if (isJoseInfrastructureError(error)) {
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
throw new APIError("UNAUTHORIZED", {
|
|
322
|
+
message: "invalid access token",
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
throw new Error(error as unknown as string);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Remote verify
|
|
334
|
+
if (opts?.remoteVerify) {
|
|
335
|
+
const { data: introspect, error: introspectError } =
|
|
336
|
+
await fetchRefusingRedirects<
|
|
337
|
+
JWTPayload & {
|
|
338
|
+
active: boolean;
|
|
339
|
+
}
|
|
340
|
+
>(opts.remoteVerify.introspectUrl, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: {
|
|
343
|
+
Accept: "application/json",
|
|
344
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
345
|
+
},
|
|
346
|
+
body: new URLSearchParams({
|
|
347
|
+
client_id: opts.remoteVerify.clientId,
|
|
348
|
+
client_secret: opts.remoteVerify.clientSecret,
|
|
349
|
+
token,
|
|
350
|
+
token_type_hint: "access_token",
|
|
351
|
+
}).toString(),
|
|
352
|
+
});
|
|
353
|
+
if (introspectError)
|
|
354
|
+
logger.error(
|
|
355
|
+
`Introspection failed: ${introspectError.message ?? introspectError.statusText}`,
|
|
356
|
+
);
|
|
357
|
+
if (!introspect)
|
|
358
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
359
|
+
message: "introspection failed",
|
|
360
|
+
});
|
|
361
|
+
if (!introspect.active)
|
|
362
|
+
throw new APIError("UNAUTHORIZED", {
|
|
363
|
+
message: "token inactive",
|
|
364
|
+
});
|
|
365
|
+
// Verifies payload using verify options (token valid through introspect).
|
|
366
|
+
// Audience is enforced by default: when `verifyOptions.audience` is set
|
|
367
|
+
// but the introspection response omits `aud` (or it mismatches),
|
|
368
|
+
// `UnsecuredJWT.decode` throws and the token is rejected. Otherwise a
|
|
369
|
+
// token issued for a different resource/client on the same issuer would
|
|
370
|
+
// also pass. Only drop the audience check when the caller has explicitly
|
|
371
|
+
// opted in via `remoteVerify.allowMissingAudience`.
|
|
372
|
+
try {
|
|
373
|
+
const unsecuredJwt = new UnsecuredJWT(introspect).encode();
|
|
374
|
+
const { audience: _audience, ...verifyOptionsNoAudience } =
|
|
375
|
+
opts.verifyOptions;
|
|
376
|
+
const skipAudience =
|
|
377
|
+
!introspect.aud && opts.remoteVerify.allowMissingAudience === true;
|
|
378
|
+
const verify = UnsecuredJWT.decode(
|
|
379
|
+
unsecuredJwt,
|
|
380
|
+
skipAudience ? verifyOptionsNoAudience : opts.verifyOptions,
|
|
381
|
+
);
|
|
382
|
+
payload = verify.payload;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new Error(error as unknown as string);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!payload)
|
|
389
|
+
throw new APIError("UNAUTHORIZED", {
|
|
390
|
+
message: `no token payload`,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Check scopes if provided
|
|
394
|
+
if (opts.scopes) {
|
|
395
|
+
const validScopes = new Set(
|
|
396
|
+
(payload.scope as string | undefined)?.split(" "),
|
|
397
|
+
);
|
|
398
|
+
for (const sc of opts.scopes) {
|
|
399
|
+
if (!validScopes.has(sc)) {
|
|
400
|
+
throw new APIError("FORBIDDEN", {
|
|
401
|
+
message: `invalid scope ${sc}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return payload;
|
|
408
|
+
}
|