@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { encodeBasicCredentials } from "./basic-credentials";
|
|
2
|
+
import type {
|
|
3
|
+
ClientAssertionGetter,
|
|
4
|
+
ClientAssertionGrantType,
|
|
5
|
+
} from "./client-assertion";
|
|
6
|
+
import { resolveClientAssertionParams } from "./client-assertion";
|
|
7
|
+
import { getPrimaryClientId } from "./utils";
|
|
8
|
+
|
|
9
|
+
export type TokenEndpointAuth =
|
|
10
|
+
| {
|
|
11
|
+
method: "none";
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
method: "client_secret_basic";
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
method: "client_secret_post";
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
method: "private_key_jwt";
|
|
21
|
+
getClientAssertion: ClientAssertionGetter;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TokenEndpointAuthMethod = TokenEndpointAuth["method"];
|
|
25
|
+
|
|
26
|
+
export type TokenEndpointSecretAuthentication = "basic" | "post";
|
|
27
|
+
|
|
28
|
+
export interface TokenEndpointClientOptions {
|
|
29
|
+
clientId?: string | string[] | undefined;
|
|
30
|
+
clientSecret?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ApplyTokenEndpointAuthInput {
|
|
34
|
+
body: URLSearchParams;
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
options: TokenEndpointClientOptions;
|
|
37
|
+
tokenEndpoint: string;
|
|
38
|
+
grantType: ClientAssertionGrantType;
|
|
39
|
+
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
40
|
+
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDefaultTokenEndpointAuth(
|
|
44
|
+
options: TokenEndpointClientOptions,
|
|
45
|
+
authentication?: TokenEndpointSecretAuthentication,
|
|
46
|
+
): TokenEndpointAuth {
|
|
47
|
+
if (authentication === "basic") {
|
|
48
|
+
return { method: "client_secret_basic" };
|
|
49
|
+
}
|
|
50
|
+
if (options.clientSecret) {
|
|
51
|
+
return { method: "client_secret_post" };
|
|
52
|
+
}
|
|
53
|
+
return { method: "none" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertNoClientSecret(
|
|
57
|
+
method: "none" | "private_key_jwt",
|
|
58
|
+
options: TokenEndpointClientOptions,
|
|
59
|
+
body: URLSearchParams,
|
|
60
|
+
) {
|
|
61
|
+
if (options.clientSecret || body.has("client_secret")) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${method} token endpoint authentication cannot be combined with clientSecret`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setClientId(body: URLSearchParams, clientId: string | undefined) {
|
|
69
|
+
if (clientId) {
|
|
70
|
+
body.set("client_id", clientId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertClientSecretConfigured(
|
|
75
|
+
method: "client_secret_basic" | "client_secret_post",
|
|
76
|
+
options: TokenEndpointClientOptions,
|
|
77
|
+
): asserts options is TokenEndpointClientOptions & { clientSecret: string } {
|
|
78
|
+
if (!options.clientSecret) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${method} token endpoint authentication requires clientSecret`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertClientIdConfigured(
|
|
86
|
+
method: TokenEndpointAuthMethod,
|
|
87
|
+
clientId: string | undefined,
|
|
88
|
+
): asserts clientId is string {
|
|
89
|
+
if (!clientId) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${method} token endpoint authentication requires clientId`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setClientSecretPostAuth({
|
|
97
|
+
body,
|
|
98
|
+
options,
|
|
99
|
+
clientId,
|
|
100
|
+
requireClientSecret,
|
|
101
|
+
}: {
|
|
102
|
+
body: URLSearchParams;
|
|
103
|
+
options: TokenEndpointClientOptions;
|
|
104
|
+
clientId: string | undefined;
|
|
105
|
+
requireClientSecret?: boolean | undefined;
|
|
106
|
+
}) {
|
|
107
|
+
if (requireClientSecret) {
|
|
108
|
+
assertClientSecretConfigured("client_secret_post", options);
|
|
109
|
+
}
|
|
110
|
+
if (options.clientSecret) {
|
|
111
|
+
assertClientIdConfigured("client_secret_post", clientId);
|
|
112
|
+
setClientId(body, clientId);
|
|
113
|
+
body.set("client_secret", options.clientSecret);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function setClientSecretBasicAuth({
|
|
118
|
+
headers,
|
|
119
|
+
options,
|
|
120
|
+
clientId,
|
|
121
|
+
body,
|
|
122
|
+
}: {
|
|
123
|
+
headers: Record<string, string>;
|
|
124
|
+
options: TokenEndpointClientOptions;
|
|
125
|
+
clientId: string | undefined;
|
|
126
|
+
body: URLSearchParams;
|
|
127
|
+
}) {
|
|
128
|
+
if (body.has("client_secret")) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"client_secret_basic token endpoint authentication cannot be combined with client_secret body parameters",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
assertClientSecretConfigured("client_secret_basic", options);
|
|
134
|
+
assertClientIdConfigured("client_secret_basic", clientId);
|
|
135
|
+
headers.authorization = encodeBasicCredentials(
|
|
136
|
+
clientId,
|
|
137
|
+
options.clientSecret,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertCompleteManualClientAssertion(body: URLSearchParams) {
|
|
142
|
+
if (body.has("client_assertion") !== body.has("client_assertion_type")) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"client_assertion and client_assertion_type must both be provided",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function applyTokenEndpointAuth({
|
|
150
|
+
body,
|
|
151
|
+
headers,
|
|
152
|
+
options,
|
|
153
|
+
tokenEndpoint,
|
|
154
|
+
grantType,
|
|
155
|
+
tokenEndpointAuth,
|
|
156
|
+
authentication,
|
|
157
|
+
}: ApplyTokenEndpointAuthInput) {
|
|
158
|
+
assertCompleteManualClientAssertion(body);
|
|
159
|
+
|
|
160
|
+
const clientId = getPrimaryClientId(options.clientId);
|
|
161
|
+
if (body.has("client_assertion")) {
|
|
162
|
+
if (tokenEndpointAuth) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"client_assertion body parameters cannot be combined with tokenEndpointAuth",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
assertNoClientSecret("private_key_jwt", options, body);
|
|
168
|
+
setClientId(body, clientId);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const auth =
|
|
173
|
+
tokenEndpointAuth ?? getDefaultTokenEndpointAuth(options, authentication);
|
|
174
|
+
|
|
175
|
+
if (auth.method === "private_key_jwt") {
|
|
176
|
+
assertNoClientSecret(auth.method, options, body);
|
|
177
|
+
assertClientIdConfigured(auth.method, clientId);
|
|
178
|
+
if (!tokenEndpoint) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"private_key_jwt token endpoint authentication requires tokenEndpoint",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const assertionParams = await resolveClientAssertionParams({
|
|
184
|
+
getClientAssertion: auth.getClientAssertion,
|
|
185
|
+
context: {
|
|
186
|
+
clientId,
|
|
187
|
+
tokenEndpoint,
|
|
188
|
+
grantType,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
setClientId(body, clientId);
|
|
192
|
+
for (const [key, value] of Object.entries(assertionParams)) {
|
|
193
|
+
body.set(key, value);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (auth.method === "none") {
|
|
199
|
+
assertNoClientSecret(auth.method, options, body);
|
|
200
|
+
if (grantType === "client_credentials") {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"none token endpoint authentication cannot be used with client_credentials grant",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
assertClientIdConfigured(auth.method, clientId);
|
|
206
|
+
setClientId(body, clientId);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (auth.method === "client_secret_basic") {
|
|
211
|
+
setClientSecretBasicAuth({ headers, options, clientId, body });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setClientSecretPostAuth({
|
|
216
|
+
body,
|
|
217
|
+
options,
|
|
218
|
+
clientId,
|
|
219
|
+
requireClientSecret: tokenEndpointAuth?.method === "client_secret_post",
|
|
220
|
+
});
|
|
221
|
+
}
|
package/src/oauth2/utils.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { base64Url } from "@better-auth/utils/base64";
|
|
2
2
|
import type { OAuth2Tokens } from "./oauth-provider";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Parse a provider's `scope` token-response field into a string array.
|
|
6
|
+
*
|
|
7
|
+
* RFC 6749 Section 3.3 defines `scope` as a space-delimited string, but
|
|
8
|
+
* providers vary: some return an already-split array. Accept both forms and
|
|
9
|
+
* drop empty or non-string entries.
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/better-auth/better-auth/issues/9076
|
|
12
|
+
*/
|
|
13
|
+
export function parseScopeField(scope: unknown): string[] {
|
|
14
|
+
if (Array.isArray(scope)) {
|
|
15
|
+
return scope
|
|
16
|
+
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
if (typeof scope === "string") {
|
|
20
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
5
26
|
const getDate = (seconds: number) => {
|
|
6
27
|
const now = new Date();
|
|
@@ -17,17 +38,63 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
|
17
38
|
refreshTokenExpiresAt: data.refresh_token_expires_in
|
|
18
39
|
? getDate(data.refresh_token_expires_in)
|
|
19
40
|
: undefined,
|
|
20
|
-
scopes: data
|
|
21
|
-
? typeof data.scope === "string"
|
|
22
|
-
? data.scope.split(" ")
|
|
23
|
-
: data.scope
|
|
24
|
-
: [],
|
|
41
|
+
scopes: parseScopeField(data.scope),
|
|
25
42
|
idToken: data.id_token,
|
|
26
43
|
// Preserve the raw token response for provider-specific fields
|
|
27
44
|
raw: data,
|
|
28
45
|
};
|
|
29
46
|
}
|
|
30
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Fill in `accessTokenExpiresAt` from the provider's configured
|
|
50
|
+
* `accessTokenExpiresIn` when the token response omitted `expires_in`. Without a
|
|
51
|
+
* known expiry, `getAccessToken` cannot tell the token is expired and never
|
|
52
|
+
* refreshes it. No-op when the provider already supplied an expiry or no
|
|
53
|
+
* fallback is configured.
|
|
54
|
+
*/
|
|
55
|
+
export function applyDefaultAccessTokenExpiry(
|
|
56
|
+
tokens: OAuth2Tokens,
|
|
57
|
+
accessTokenExpiresIn: number | undefined,
|
|
58
|
+
): OAuth2Tokens {
|
|
59
|
+
if (!tokens.accessTokenExpiresAt && accessTokenExpiresIn) {
|
|
60
|
+
tokens.accessTokenExpiresAt = new Date(
|
|
61
|
+
Date.now() + accessTokenExpiresIn * 1000,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute the union of stored and incoming OAuth scopes, preserving
|
|
69
|
+
* stored insertion order and dropping duplicates.
|
|
70
|
+
*/
|
|
71
|
+
export function mergeScopes(
|
|
72
|
+
stored: string | null | undefined,
|
|
73
|
+
incoming: string[] | undefined,
|
|
74
|
+
): string {
|
|
75
|
+
const existing = stored
|
|
76
|
+
? stored
|
|
77
|
+
.split(",")
|
|
78
|
+
.map((scope) => scope.trim())
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
: [];
|
|
81
|
+
const next = (incoming ?? []).map((scope) => scope.trim()).filter(Boolean);
|
|
82
|
+
return [...new Set([...existing, ...next])].join(",");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return the provider's primary Client ID: the single string, or the entry at
|
|
87
|
+
* array index 0 for the cross-platform form used by ID token audience
|
|
88
|
+
* verification. Index 0 is the designated primary and pairs with
|
|
89
|
+
* `clientSecret` for the authorization code flow; later array entries are
|
|
90
|
+
* only used as additional accepted audiences. Returns `undefined` when the
|
|
91
|
+
* primary value is missing or an empty string.
|
|
92
|
+
*/
|
|
93
|
+
export function getPrimaryClientId(clientId: unknown): string | undefined {
|
|
94
|
+
const value = Array.isArray(clientId) ? clientId[0] : clientId;
|
|
95
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
31
98
|
export async function generateCodeChallenge(codeVerifier: string) {
|
|
32
99
|
const encoder = new TextEncoder();
|
|
33
100
|
const data = encoder.encode(codeVerifier);
|
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
1
|
+
import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
|
|
4
2
|
import type { AwaitableFunction } from "../types";
|
|
5
|
-
import type { ClientAssertionConfig } from "./client-assertion";
|
|
6
|
-
import { resolveAssertionParams } from "./client-assertion";
|
|
7
3
|
import type { ProviderOptions } from "./index";
|
|
8
4
|
import { getOAuth2Tokens } from "./index";
|
|
5
|
+
import {
|
|
6
|
+
assertResponseNotRedirect,
|
|
7
|
+
fetchRefusingRedirects,
|
|
8
|
+
noFollowRedirect,
|
|
9
|
+
} from "./reject-redirects";
|
|
10
|
+
import type {
|
|
11
|
+
TokenEndpointAuth,
|
|
12
|
+
TokenEndpointSecretAuthentication,
|
|
13
|
+
} from "./token-endpoint-auth";
|
|
14
|
+
import { applyTokenEndpointAuth } from "./token-endpoint-auth";
|
|
15
|
+
|
|
16
|
+
interface AuthorizationCodeRequestInput {
|
|
17
|
+
code: string;
|
|
18
|
+
redirectURI: string;
|
|
19
|
+
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
20
|
+
codeVerifier?: string | undefined;
|
|
21
|
+
deviceId?: string | undefined;
|
|
22
|
+
authentication?: TokenEndpointSecretAuthentication | undefined;
|
|
23
|
+
tokenEndpointAuth?: TokenEndpointAuth | undefined;
|
|
24
|
+
tokenEndpoint?: string | undefined;
|
|
25
|
+
headers?: Record<string, string> | undefined;
|
|
26
|
+
additionalParams?: Record<string, string> | undefined;
|
|
27
|
+
resource?: (string | string[]) | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AuthorizationCodeRequestBaseInput {
|
|
31
|
+
code: string;
|
|
32
|
+
redirectURI: string;
|
|
33
|
+
options: Partial<ProviderOptions>;
|
|
34
|
+
codeVerifier?: string | undefined;
|
|
35
|
+
deviceId?: string | undefined;
|
|
36
|
+
headers?: Record<string, string> | undefined;
|
|
37
|
+
additionalParams?: Record<string, string> | undefined;
|
|
38
|
+
resource?: (string | string[]) | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ValidateAuthorizationCodeInput extends AuthorizationCodeRequestInput {
|
|
42
|
+
tokenEndpoint: string;
|
|
43
|
+
}
|
|
9
44
|
|
|
10
45
|
export async function authorizationCodeRequest({
|
|
11
46
|
code,
|
|
@@ -13,84 +48,50 @@ export async function authorizationCodeRequest({
|
|
|
13
48
|
redirectURI,
|
|
14
49
|
options,
|
|
15
50
|
authentication,
|
|
16
|
-
|
|
51
|
+
tokenEndpointAuth,
|
|
17
52
|
tokenEndpoint,
|
|
18
53
|
deviceId,
|
|
19
54
|
headers,
|
|
20
55
|
additionalParams = {},
|
|
21
56
|
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" | "private_key_jwt") | undefined;
|
|
29
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
30
|
-
/** Token endpoint URL. Used as the JWT `aud` claim when signing assertions. */
|
|
31
|
-
tokenEndpoint?: string | undefined;
|
|
32
|
-
headers?: Record<string, string> | undefined;
|
|
33
|
-
additionalParams?: Record<string, string> | undefined;
|
|
34
|
-
resource?: (string | string[]) | undefined;
|
|
35
|
-
}) {
|
|
57
|
+
}: AuthorizationCodeRequestInput) {
|
|
36
58
|
options = typeof options === "function" ? await options() : options;
|
|
37
|
-
|
|
38
|
-
if (authentication === "private_key_jwt") {
|
|
39
|
-
if (!clientAssertion) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
"private_key_jwt authentication requires a clientAssertion configuration",
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
const primaryClientId = Array.isArray(options.clientId)
|
|
45
|
-
? options.clientId[0]
|
|
46
|
-
: options.clientId;
|
|
47
|
-
const assertionParams = await resolveAssertionParams({
|
|
48
|
-
clientAssertion,
|
|
49
|
-
clientId: primaryClientId,
|
|
50
|
-
tokenEndpoint,
|
|
51
|
-
});
|
|
52
|
-
additionalParams = { ...additionalParams, ...assertionParams };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return createAuthorizationCodeRequest({
|
|
59
|
+
const request = buildAuthorizationCodeRequest({
|
|
56
60
|
code,
|
|
57
61
|
codeVerifier,
|
|
58
62
|
redirectURI,
|
|
59
63
|
options,
|
|
60
|
-
authentication,
|
|
61
64
|
deviceId,
|
|
62
65
|
headers,
|
|
63
66
|
additionalParams,
|
|
64
67
|
resource,
|
|
65
68
|
});
|
|
69
|
+
|
|
70
|
+
await applyTokenEndpointAuth({
|
|
71
|
+
body: request.body,
|
|
72
|
+
headers: request.headers,
|
|
73
|
+
options,
|
|
74
|
+
tokenEndpoint: tokenEndpoint ?? "",
|
|
75
|
+
grantType: "authorization_code",
|
|
76
|
+
tokenEndpointAuth,
|
|
77
|
+
authentication,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return request;
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
* @deprecated use async'd authorizationCodeRequest instead
|
|
70
|
-
*/
|
|
71
|
-
export function createAuthorizationCodeRequest({
|
|
83
|
+
function buildAuthorizationCodeRequest({
|
|
72
84
|
code,
|
|
73
85
|
codeVerifier,
|
|
74
86
|
redirectURI,
|
|
75
87
|
options,
|
|
76
|
-
authentication,
|
|
77
88
|
deviceId,
|
|
78
89
|
headers,
|
|
79
90
|
additionalParams = {},
|
|
80
91
|
resource,
|
|
81
|
-
}: {
|
|
82
|
-
code: string;
|
|
83
|
-
redirectURI: string;
|
|
84
|
-
options: Partial<ProviderOptions>;
|
|
85
|
-
codeVerifier?: string | undefined;
|
|
86
|
-
deviceId?: string | undefined;
|
|
87
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
88
|
-
headers?: Record<string, string> | undefined;
|
|
89
|
-
additionalParams?: Record<string, string> | undefined;
|
|
90
|
-
resource?: (string | string[]) | undefined;
|
|
91
|
-
}) {
|
|
92
|
+
}: AuthorizationCodeRequestBaseInput) {
|
|
92
93
|
const body = new URLSearchParams();
|
|
93
|
-
const requestHeaders: Record<string,
|
|
94
|
+
const requestHeaders: Record<string, string> = {
|
|
94
95
|
"content-type": "application/x-www-form-urlencoded",
|
|
95
96
|
accept: "application/json",
|
|
96
97
|
...headers,
|
|
@@ -111,21 +112,6 @@ export function createAuthorizationCodeRequest({
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
|
-
const primaryClientId = Array.isArray(options.clientId)
|
|
115
|
-
? options.clientId[0]
|
|
116
|
-
: options.clientId;
|
|
117
|
-
if (authentication === "basic") {
|
|
118
|
-
const encodedCredentials = base64.encode(
|
|
119
|
-
`${primaryClientId}:${options.clientSecret ?? ""}`,
|
|
120
|
-
);
|
|
121
|
-
requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
|
|
122
|
-
} else {
|
|
123
|
-
body.set("client_id", primaryClientId);
|
|
124
|
-
if (authentication !== "private_key_jwt" && options.clientSecret) {
|
|
125
|
-
body.set("client_secret", options.clientSecret);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
115
|
for (const [key, value] of Object.entries(additionalParams)) {
|
|
130
116
|
if (!body.has(key)) body.append(key, value);
|
|
131
117
|
}
|
|
@@ -143,31 +129,19 @@ export async function validateAuthorizationCode({
|
|
|
143
129
|
options,
|
|
144
130
|
tokenEndpoint,
|
|
145
131
|
authentication,
|
|
146
|
-
|
|
132
|
+
tokenEndpointAuth,
|
|
147
133
|
deviceId,
|
|
148
134
|
headers,
|
|
149
135
|
additionalParams = {},
|
|
150
136
|
resource,
|
|
151
|
-
}: {
|
|
152
|
-
code: string;
|
|
153
|
-
redirectURI: string;
|
|
154
|
-
options: AwaitableFunction<Partial<ProviderOptions>>;
|
|
155
|
-
codeVerifier?: string | undefined;
|
|
156
|
-
deviceId?: string | undefined;
|
|
157
|
-
tokenEndpoint: string;
|
|
158
|
-
authentication?: ("basic" | "post" | "private_key_jwt") | undefined;
|
|
159
|
-
clientAssertion?: ClientAssertionConfig | undefined;
|
|
160
|
-
headers?: Record<string, string> | undefined;
|
|
161
|
-
additionalParams?: Record<string, string> | undefined;
|
|
162
|
-
resource?: (string | string[]) | undefined;
|
|
163
|
-
}) {
|
|
137
|
+
}: ValidateAuthorizationCodeInput) {
|
|
164
138
|
const { body, headers: requestHeaders } = await authorizationCodeRequest({
|
|
165
139
|
code,
|
|
166
140
|
codeVerifier,
|
|
167
141
|
redirectURI,
|
|
168
142
|
options,
|
|
169
143
|
authentication,
|
|
170
|
-
|
|
144
|
+
tokenEndpointAuth,
|
|
171
145
|
tokenEndpoint,
|
|
172
146
|
deviceId,
|
|
173
147
|
headers,
|
|
@@ -175,7 +149,7 @@ export async function validateAuthorizationCode({
|
|
|
175
149
|
resource,
|
|
176
150
|
});
|
|
177
151
|
|
|
178
|
-
const { data, error } = await
|
|
152
|
+
const { data, error } = await fetchRefusingRedirects<object>(tokenEndpoint, {
|
|
179
153
|
method: "POST",
|
|
180
154
|
body: body,
|
|
181
155
|
headers: requestHeaders,
|
|
@@ -195,7 +169,13 @@ export async function validateToken(
|
|
|
195
169
|
issuer?: string | string[];
|
|
196
170
|
},
|
|
197
171
|
) {
|
|
198
|
-
const jwks = createRemoteJWKSet(new URL(jwksEndpoint)
|
|
172
|
+
const jwks = createRemoteJWKSet(new URL(jwksEndpoint), {
|
|
173
|
+
[customFetch]: async (url, init) => {
|
|
174
|
+
const response = await fetch(url, { ...init, ...noFollowRedirect });
|
|
175
|
+
assertResponseNotRedirect(String(url), response);
|
|
176
|
+
return response;
|
|
177
|
+
},
|
|
178
|
+
});
|
|
199
179
|
const verified = await jwtVerify(token, jwks, {
|
|
200
180
|
audience: options?.audience,
|
|
201
181
|
issuer: options?.issuer,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { decodeProtectedHeader, jwtVerify } from "jose";
|
|
2
|
+
import type { OAuthProvider, ProviderOptions } from "./oauth-provider";
|
|
3
|
+
|
|
4
|
+
type ProviderWithIdTokenConfig = Pick<OAuthProvider, "idToken" | "options">;
|
|
5
|
+
|
|
6
|
+
async function sha256Hex(value: string) {
|
|
7
|
+
const data = new TextEncoder().encode(value);
|
|
8
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
9
|
+
return Array.from(new Uint8Array(digest))
|
|
10
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
11
|
+
.join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function nonceMatches(
|
|
15
|
+
claimNonce: unknown,
|
|
16
|
+
nonce: string,
|
|
17
|
+
comparison: "exact" | "exact-or-sha256" = "exact",
|
|
18
|
+
) {
|
|
19
|
+
if (typeof claimNonce !== "string") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (claimNonce === nonce) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (comparison === "exact-or-sha256") {
|
|
26
|
+
return claimNonce === (await sha256Hex(nonce));
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a provider can verify a client-submitted id_token.
|
|
33
|
+
*
|
|
34
|
+
* A provider supports id_token sign-in when it declares an {@link OAuthProvider.idToken}
|
|
35
|
+
* verification config, or when the integrator supplies a `verifyIdToken` override on the
|
|
36
|
+
* provider options. A provider whose options set `disableIdTokenSignIn`, or that declares
|
|
37
|
+
* neither, rejects the client id_token sign-in path with `ID_TOKEN_NOT_SUPPORTED`.
|
|
38
|
+
*/
|
|
39
|
+
export function supportsIdTokenSignIn(provider: ProviderWithIdTokenConfig) {
|
|
40
|
+
const options = (provider.options ?? {}) as Partial<ProviderOptions>;
|
|
41
|
+
if (options.disableIdTokenSignIn) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return Boolean(provider.idToken || options.verifyIdToken);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Verify a client-submitted id_token against a provider's verification config.
|
|
49
|
+
*
|
|
50
|
+
* This is the single id_token verifier for every social provider. Providers no longer
|
|
51
|
+
* implement their own boolean `verifyIdToken`; they declare an {@link OAuthProvider.idToken}
|
|
52
|
+
* config and this function performs the cryptographic check. The contract is fail-closed: a
|
|
53
|
+
* provider without a config (and without an integrator `verifyIdToken` override) returns
|
|
54
|
+
* `false`, so a forged token can never be accepted by omission.
|
|
55
|
+
*
|
|
56
|
+
* @returns `true` only when the token is authentic for the provider.
|
|
57
|
+
*/
|
|
58
|
+
export async function verifyProviderIdToken(
|
|
59
|
+
provider: ProviderWithIdTokenConfig,
|
|
60
|
+
token: string,
|
|
61
|
+
nonce?: string,
|
|
62
|
+
): Promise<boolean> {
|
|
63
|
+
const options = (provider.options ?? {}) as Partial<ProviderOptions>;
|
|
64
|
+
if (options.disableIdTokenSignIn) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// Every verification path is fail-closed: a throw from the integrator override, a custom
|
|
68
|
+
// remote verifier, the JWKS resolver, or signature checking resolves to `false` instead of
|
|
69
|
+
// escaping to the caller as a server error.
|
|
70
|
+
try {
|
|
71
|
+
if (options.verifyIdToken) {
|
|
72
|
+
return await options.verifyIdToken(token, nonce);
|
|
73
|
+
}
|
|
74
|
+
const config = provider.idToken;
|
|
75
|
+
if (!config) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if ("verify" in config) {
|
|
79
|
+
return await config.verify(token, nonce);
|
|
80
|
+
}
|
|
81
|
+
// Opaque (non-JWS) tokens carry no signature to check. They are accepted only when the
|
|
82
|
+
// provider opts in, in which case getUserInfo resolves identity from the access token via
|
|
83
|
+
// the provider's userinfo endpoint, which validates it (e.g. Facebook Graph access tokens).
|
|
84
|
+
// An expected `nonce` is not enforced here: an opaque token carries no `nonce` claim, and the
|
|
85
|
+
// access-token-backed userinfo exchange (not the token itself) is the identity source.
|
|
86
|
+
if (token.split(".").length !== 3) {
|
|
87
|
+
return config.allowOpaqueToken === true;
|
|
88
|
+
}
|
|
89
|
+
// `kid` is optional in JWS: a JWKS resolver can still select a key by algorithm, so
|
|
90
|
+
// key selection is left to config.jwks. The token's `alg` only seeds the default
|
|
91
|
+
// allowed-algorithms list when the provider does not pin one.
|
|
92
|
+
const { alg } = decodeProtectedHeader(token);
|
|
93
|
+
const { payload } = await jwtVerify(token, config.jwks, {
|
|
94
|
+
issuer: config.issuer,
|
|
95
|
+
audience: config.audience,
|
|
96
|
+
algorithms: config.algorithms ?? (alg ? [alg] : undefined),
|
|
97
|
+
maxTokenAge: config.maxTokenAge,
|
|
98
|
+
});
|
|
99
|
+
if (
|
|
100
|
+
nonce &&
|
|
101
|
+
!(await nonceMatches(payload.nonce, nonce, config.nonceComparison))
|
|
102
|
+
) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
// Provider-specific claim check on the now-verified payload (e.g. Google's
|
|
106
|
+
// hosted-domain `hd`). Standard checks have already passed, so a token that
|
|
107
|
+
// fails here is authentic but does not meet the provider's extra constraint.
|
|
108
|
+
if (config.verifyClaims && !config.verifyClaims(payload)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|