better-auth 1.6.9 → 1.6.11
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 +0 -2
- package/dist/api/routes/callback.mjs +6 -5
- package/dist/api/routes/email-verification.mjs +2 -2
- package/dist/api/routes/error.mjs +1 -1
- package/dist/api/routes/sign-in.d.mts +0 -1
- package/dist/api/routes/sign-in.mjs +4 -11
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +1 -1
- package/dist/api/to-auth-endpoints.mjs +7 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/plugins/index.d.mts +2 -1
- package/dist/cookies/cookie-utils.d.mts +10 -1
- package/dist/cookies/cookie-utils.mjs +19 -1
- package/dist/cookies/index.d.mts +2 -2
- package/dist/cookies/index.mjs +2 -2
- package/dist/db/internal-adapter.mjs +103 -7
- package/dist/db/with-hooks.d.mts +1 -0
- package/dist/db/with-hooks.mjs +58 -1
- package/dist/integrations/cookie-plugin-guard.mjs +18 -0
- package/dist/integrations/next-js.mjs +6 -0
- package/dist/integrations/svelte-kit.mjs +6 -0
- package/dist/integrations/tanstack-start-solid.mjs +6 -0
- package/dist/integrations/tanstack-start.mjs +6 -0
- package/dist/oauth2/link-account.mjs +3 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.d.mts +3 -15
- package/dist/plugins/access/index.d.mts +2 -2
- package/dist/plugins/access/types.d.mts +11 -4
- package/dist/plugins/admin/access/statement.d.mts +29 -93
- package/dist/plugins/admin/client.d.mts +5 -0
- package/dist/plugins/admin/client.mjs +5 -0
- package/dist/plugins/admin/routes.mjs +1 -0
- package/dist/plugins/anonymous/client.d.mts +1 -0
- package/dist/plugins/anonymous/error-codes.d.mts +1 -0
- package/dist/plugins/anonymous/error-codes.mjs +1 -0
- package/dist/plugins/anonymous/index.d.mts +1 -0
- package/dist/plugins/anonymous/index.mjs +16 -2
- package/dist/plugins/bearer/index.mjs +2 -4
- package/dist/plugins/captcha/index.mjs +14 -1
- package/dist/plugins/device-authorization/error-codes.mjs +1 -0
- package/dist/plugins/device-authorization/index.d.mts +1 -0
- package/dist/plugins/device-authorization/routes.mjs +34 -3
- package/dist/plugins/email-otp/routes.mjs +3 -3
- package/dist/plugins/generic-oauth/routes.mjs +3 -3
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/magic-link/index.d.mts +8 -1
- package/dist/plugins/magic-link/index.mjs +5 -17
- package/dist/plugins/mcp/authorize.mjs +8 -2
- package/dist/plugins/mcp/index.mjs +73 -30
- package/dist/plugins/oidc-provider/authorize.mjs +8 -2
- package/dist/plugins/oidc-provider/index.mjs +63 -33
- package/dist/plugins/one-tap/index.mjs +16 -10
- package/dist/plugins/organization/access/statement.d.mts +68 -201
- package/dist/plugins/organization/client.d.mts +1 -0
- package/dist/plugins/organization/client.mjs +1 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +1 -0
- package/dist/plugins/organization/routes/crud-access-control.d.mts +2 -2
- package/dist/plugins/organization/routes/crud-invites.d.mts +8 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +5 -3
- package/dist/plugins/organization/routes/crud-team.mjs +7 -2
- package/dist/plugins/organization/types.d.mts +12 -2
- package/dist/plugins/siwe/client.d.mts +4 -0
- package/dist/plugins/siwe/client.mjs +5 -1
- package/dist/plugins/siwe/index.d.mts +13 -2
- package/dist/plugins/siwe/index.mjs +179 -165
- package/dist/plugins/username/index.d.mts +11 -0
- package/dist/plugins/username/index.mjs +18 -2
- package/dist/test-utils/test-instance.d.mts +1 -6
- package/dist/test-utils/test-instance.mjs +11 -2
- package/package.json +10 -10
|
@@ -27,6 +27,7 @@ const magicLink = (options) => {
|
|
|
27
27
|
allowedAttempts: 1,
|
|
28
28
|
...options
|
|
29
29
|
};
|
|
30
|
+
if (options.allowedAttempts !== void 0 && options.allowedAttempts !== 1) console.warn("[better-auth/magic-link] `allowedAttempts` is ignored: tokens are consumed atomically on the first verification call (GHSA-hc7v-rggr-4hvx). Any value other than `1` has no effect; remove the option to silence this warning.");
|
|
30
31
|
async function storeToken(ctx, token) {
|
|
31
32
|
if (opts.storeToken === "hashed") return await defaultKeyHasher(token);
|
|
32
33
|
if (typeof opts.storeToken === "object" && "type" in opts.storeToken && opts.storeToken.type === "custom-hasher") return await opts.storeToken.hash(token);
|
|
@@ -59,8 +60,7 @@ const magicLink = (options) => {
|
|
|
59
60
|
identifier: storedToken,
|
|
60
61
|
value: JSON.stringify({
|
|
61
62
|
email,
|
|
62
|
-
name: ctx.body.name
|
|
63
|
-
attempt: 0
|
|
63
|
+
name: ctx.body.name
|
|
64
64
|
}),
|
|
65
65
|
expiresAt: new Date(Date.now() + (opts.expiresIn || 300) * 1e3)
|
|
66
66
|
});
|
|
@@ -119,22 +119,10 @@ const magicLink = (options) => {
|
|
|
119
119
|
}
|
|
120
120
|
const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : callbackURL, ctx.context.baseURL).toString();
|
|
121
121
|
const storedToken = await storeToken(ctx, token);
|
|
122
|
-
const tokenValue = await ctx.context.internalAdapter.
|
|
122
|
+
const tokenValue = await ctx.context.internalAdapter.consumeVerificationValue(storedToken);
|
|
123
123
|
if (!tokenValue) redirectWithError("INVALID_TOKEN");
|
|
124
|
-
if (tokenValue.expiresAt < /* @__PURE__ */ new Date())
|
|
125
|
-
|
|
126
|
-
redirectWithError("EXPIRED_TOKEN");
|
|
127
|
-
}
|
|
128
|
-
const { email, name, attempt = 0 } = JSON.parse(tokenValue.value);
|
|
129
|
-
if (attempt >= opts.allowedAttempts) {
|
|
130
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(storedToken);
|
|
131
|
-
redirectWithError("ATTEMPTS_EXCEEDED");
|
|
132
|
-
}
|
|
133
|
-
await ctx.context.internalAdapter.updateVerificationByIdentifier(storedToken, { value: JSON.stringify({
|
|
134
|
-
email,
|
|
135
|
-
name,
|
|
136
|
-
attempt: attempt + 1
|
|
137
|
-
}) });
|
|
124
|
+
if (tokenValue.expiresAt < /* @__PURE__ */ new Date()) redirectWithError("EXPIRED_TOKEN");
|
|
125
|
+
const { email, name } = JSON.parse(tokenValue.value);
|
|
138
126
|
let isNewUser = false;
|
|
139
127
|
let user = await ctx.context.internalAdapter.findUserByEmail(email).then((res) => res?.user);
|
|
140
128
|
if (!user) if (!opts.disableSignUp) {
|
|
@@ -72,8 +72,14 @@ async function authorizeMCPOAuth(ctx, options) {
|
|
|
72
72
|
});
|
|
73
73
|
if (invalidScopes.length) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`));
|
|
74
74
|
if ((!query.code_challenge || !query.code_challenge_method) && options.requirePKCE) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_request", "pkce is required"));
|
|
75
|
-
if (!query.
|
|
76
|
-
if (
|
|
75
|
+
if (query.code_challenge_method && !query.code_challenge) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_request", "code_challenge_method requires code_challenge"));
|
|
76
|
+
if (query.code_challenge) {
|
|
77
|
+
const allowedCodeChallengeMethods = options.allowPlainCodeChallengeMethod ? ["s256", "plain"] : ["s256"];
|
|
78
|
+
let codeChallengeMethod = query.code_challenge_method?.toLowerCase();
|
|
79
|
+
if (!codeChallengeMethod && options.allowPlainCodeChallengeMethod) codeChallengeMethod = "plain";
|
|
80
|
+
if (!codeChallengeMethod || !allowedCodeChallengeMethods.includes(codeChallengeMethod)) throw ctx.redirect(redirectErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method"));
|
|
81
|
+
query.code_challenge_method = codeChallengeMethod;
|
|
82
|
+
}
|
|
77
83
|
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
78
84
|
const codeExpiresInMs = opts.codeExpiresIn * 1e3;
|
|
79
85
|
const expiresAt = new Date(Date.now() + codeExpiresInMs);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getBaseURL, isDynamicBaseURLConfig, resolveBaseURL } from "../../utils/url.mjs";
|
|
2
2
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
3
|
+
import { constantTimeEqual } from "../../crypto/buffer.mjs";
|
|
3
4
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
5
|
import { expireCookie } from "../../cookies/index.mjs";
|
|
5
6
|
import { resolveDynamicTrustedProxyHeaders } from "../../context/helpers.mjs";
|
|
@@ -45,7 +46,7 @@ const getMCPProviderMetadata = (ctx, options) => {
|
|
|
45
46
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
46
47
|
acr_values_supported: ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"],
|
|
47
48
|
subject_types_supported: ["public"],
|
|
48
|
-
id_token_signing_alg_values_supported: ["RS256"
|
|
49
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
49
50
|
token_endpoint_auth_methods_supported: [
|
|
50
51
|
"client_secret_basic",
|
|
51
52
|
"client_secret_post",
|
|
@@ -81,7 +82,7 @@ const getMCPProtectedResourceMetadata = (ctx, options) => {
|
|
|
81
82
|
"offline_access"
|
|
82
83
|
],
|
|
83
84
|
bearer_methods_supported: ["header"],
|
|
84
|
-
resource_signing_alg_values_supported: ["RS256"
|
|
85
|
+
resource_signing_alg_values_supported: ["RS256"]
|
|
85
86
|
};
|
|
86
87
|
};
|
|
87
88
|
const registerMcpClientBodySchema = z.object({
|
|
@@ -122,7 +123,7 @@ const mcp = (options) => {
|
|
|
122
123
|
defaultScope: "openid",
|
|
123
124
|
accessTokenExpiresIn: 3600,
|
|
124
125
|
refreshTokenExpiresIn: 604800,
|
|
125
|
-
allowPlainCodeChallengeMethod:
|
|
126
|
+
allowPlainCodeChallengeMethod: false,
|
|
126
127
|
...options.oidcConfig,
|
|
127
128
|
loginPage: options.loginPage,
|
|
128
129
|
scopes: [
|
|
@@ -225,10 +226,6 @@ const mcp = (options) => {
|
|
|
225
226
|
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"]
|
|
226
227
|
}
|
|
227
228
|
}, async (ctx) => {
|
|
228
|
-
ctx.setHeader("Access-Control-Allow-Origin", "*");
|
|
229
|
-
ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
230
|
-
ctx.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
231
|
-
ctx.setHeader("Access-Control-Max-Age", "86400");
|
|
232
229
|
let { body } = ctx;
|
|
233
230
|
if (!body) throw ctx.error("BAD_REQUEST", {
|
|
234
231
|
error_description: "request body not found",
|
|
@@ -241,25 +238,43 @@ const mcp = (options) => {
|
|
|
241
238
|
});
|
|
242
239
|
let { client_id, client_secret } = body;
|
|
243
240
|
const authorization = ctx.request?.headers.get("authorization") || null;
|
|
244
|
-
if (authorization && !
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
if (authorization && !client_secret && authorization.startsWith("Basic ")) {
|
|
242
|
+
let decoded;
|
|
243
|
+
try {
|
|
244
|
+
const encoded = authorization.replace("Basic ", "");
|
|
245
|
+
decoded = new TextDecoder().decode(base64.decode(encoded));
|
|
246
|
+
} catch {
|
|
247
|
+
throw new APIError("UNAUTHORIZED", {
|
|
248
|
+
error_description: "invalid authorization header format",
|
|
249
|
+
error: "invalid_client"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const colonIndex = decoded.indexOf(":");
|
|
253
|
+
if (colonIndex === -1) throw new APIError("UNAUTHORIZED", {
|
|
248
254
|
error_description: "invalid authorization header format",
|
|
249
255
|
error: "invalid_client"
|
|
250
256
|
});
|
|
251
|
-
|
|
257
|
+
let id;
|
|
258
|
+
let secret;
|
|
259
|
+
try {
|
|
260
|
+
id = decodeURIComponent(decoded.slice(0, colonIndex));
|
|
261
|
+
secret = decodeURIComponent(decoded.slice(colonIndex + 1));
|
|
262
|
+
} catch {
|
|
263
|
+
throw new APIError("UNAUTHORIZED", {
|
|
264
|
+
error_description: "invalid authorization header format",
|
|
265
|
+
error: "invalid_client"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
252
268
|
if (!id || !secret) throw new APIError("UNAUTHORIZED", {
|
|
253
269
|
error_description: "invalid authorization header format",
|
|
254
270
|
error: "invalid_client"
|
|
255
271
|
});
|
|
256
|
-
client_id
|
|
257
|
-
|
|
258
|
-
} catch {
|
|
259
|
-
throw new APIError("UNAUTHORIZED", {
|
|
260
|
-
error_description: "invalid authorization header format",
|
|
272
|
+
if (client_id && client_id.toString() !== id) throw new APIError("UNAUTHORIZED", {
|
|
273
|
+
error_description: "client_id in body does not match Authorization header",
|
|
261
274
|
error: "invalid_client"
|
|
262
275
|
});
|
|
276
|
+
client_id = id;
|
|
277
|
+
client_secret = secret;
|
|
263
278
|
}
|
|
264
279
|
const { grant_type, code, redirect_uri, refresh_token, code_verifier } = body;
|
|
265
280
|
if (grant_type === "refresh_token") {
|
|
@@ -286,6 +301,38 @@ const mcp = (options) => {
|
|
|
286
301
|
error_description: "refresh token expired",
|
|
287
302
|
error: "invalid_grant"
|
|
288
303
|
});
|
|
304
|
+
const refreshClient = await ctx.context.adapter.findOne({
|
|
305
|
+
model: modelName.oauthClient,
|
|
306
|
+
where: [{
|
|
307
|
+
field: "clientId",
|
|
308
|
+
value: client_id.toString()
|
|
309
|
+
}]
|
|
310
|
+
}).then((res) => {
|
|
311
|
+
if (!res) return null;
|
|
312
|
+
return {
|
|
313
|
+
...res,
|
|
314
|
+
redirectUrls: res.redirectUrls.split(","),
|
|
315
|
+
metadata: res.metadata ? JSON.parse(res.metadata) : {}
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
if (!refreshClient) throw new APIError("UNAUTHORIZED", {
|
|
319
|
+
error_description: "invalid client_id",
|
|
320
|
+
error: "invalid_client"
|
|
321
|
+
});
|
|
322
|
+
if (refreshClient.disabled) throw new APIError("UNAUTHORIZED", {
|
|
323
|
+
error_description: "client is disabled",
|
|
324
|
+
error: "invalid_client"
|
|
325
|
+
});
|
|
326
|
+
if (refreshClient.type !== "public") {
|
|
327
|
+
if (!refreshClient.clientSecret || !client_secret) throw new APIError("UNAUTHORIZED", {
|
|
328
|
+
error_description: "client_secret is required for confidential clients",
|
|
329
|
+
error: "invalid_client"
|
|
330
|
+
});
|
|
331
|
+
if (!constantTimeEqual(refreshClient.clientSecret, client_secret.toString())) throw new APIError("UNAUTHORIZED", {
|
|
332
|
+
error_description: "invalid client_secret",
|
|
333
|
+
error: "invalid_client"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
289
336
|
const accessToken = generateRandomString(32, "a-z", "A-Z");
|
|
290
337
|
const newRefreshToken = generateRandomString(32, "a-z", "A-Z");
|
|
291
338
|
const accessTokenExpiresAt = new Date(Date.now() + opts.accessTokenExpiresIn * 1e3);
|
|
@@ -320,11 +367,7 @@ const mcp = (options) => {
|
|
|
320
367
|
error_description: "code verifier is missing",
|
|
321
368
|
error: "invalid_request"
|
|
322
369
|
});
|
|
323
|
-
|
|
324
|
-
* We need to check if the code is valid before we can proceed
|
|
325
|
-
* with the rest of the request.
|
|
326
|
-
*/
|
|
327
|
-
const verificationValue = await ctx.context.internalAdapter.findVerificationValue(code.toString());
|
|
370
|
+
const verificationValue = await ctx.context.internalAdapter.consumeVerificationValue(code.toString());
|
|
328
371
|
if (!verificationValue) throw new APIError("UNAUTHORIZED", {
|
|
329
372
|
error_description: "invalid code",
|
|
330
373
|
error: "invalid_grant"
|
|
@@ -333,7 +376,6 @@ const mcp = (options) => {
|
|
|
333
376
|
error_description: "code expired",
|
|
334
377
|
error: "invalid_grant"
|
|
335
378
|
});
|
|
336
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
|
|
337
379
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
338
380
|
error_description: "client_id is required",
|
|
339
381
|
error: "invalid_client"
|
|
@@ -378,11 +420,11 @@ const mcp = (options) => {
|
|
|
378
420
|
error: "invalid_request"
|
|
379
421
|
});
|
|
380
422
|
} else {
|
|
381
|
-
if (!client_secret) throw new APIError("UNAUTHORIZED", {
|
|
423
|
+
if (!client.clientSecret || !client_secret) throw new APIError("UNAUTHORIZED", {
|
|
382
424
|
error_description: "client_secret is required for confidential clients",
|
|
383
425
|
error: "invalid_client"
|
|
384
426
|
});
|
|
385
|
-
if (!(client.clientSecret
|
|
427
|
+
if (!constantTimeEqual(client.clientSecret, client_secret.toString())) throw new APIError("UNAUTHORIZED", {
|
|
386
428
|
error_description: "invalid client_secret",
|
|
387
429
|
error: "invalid_client"
|
|
388
430
|
});
|
|
@@ -400,12 +442,13 @@ const mcp = (options) => {
|
|
|
400
442
|
error_description: "code verifier is missing",
|
|
401
443
|
error: "invalid_request"
|
|
402
444
|
});
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
445
|
+
if (value.codeChallenge) {
|
|
446
|
+
if ((value.codeChallengeMethod === "plain" ? code_verifier : await createHash("SHA-256", "base64urlnopad").digest(code_verifier)) !== value.codeChallenge) throw new APIError("UNAUTHORIZED", {
|
|
447
|
+
error_description: "code verification failed",
|
|
448
|
+
error: "invalid_request"
|
|
449
|
+
});
|
|
450
|
+
}
|
|
407
451
|
const requestedScopes = value.scope;
|
|
408
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
|
|
409
452
|
const accessToken = generateRandomString(32, "a-z", "A-Z");
|
|
410
453
|
const refreshToken = generateRandomString(32, "A-Z", "a-z");
|
|
411
454
|
const accessTokenExpiresAt = new Date(Date.now() + opts.accessTokenExpiresIn * 1e3);
|
|
@@ -93,8 +93,14 @@ async function authorize(ctx, options) {
|
|
|
93
93
|
});
|
|
94
94
|
if (invalidScopes.length) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`));
|
|
95
95
|
if ((!query.code_challenge || !query.code_challenge_method) && options.requirePKCE) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required"));
|
|
96
|
-
if (!query.
|
|
97
|
-
if (
|
|
96
|
+
if (query.code_challenge_method && !query.code_challenge) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge_method requires code_challenge"));
|
|
97
|
+
if (query.code_challenge) {
|
|
98
|
+
const allowedCodeChallengeMethods = options.allowPlainCodeChallengeMethod ? ["s256", "plain"] : ["s256"];
|
|
99
|
+
let codeChallengeMethod = query.code_challenge_method?.toLowerCase();
|
|
100
|
+
if (!codeChallengeMethod && options.allowPlainCodeChallengeMethod) codeChallengeMethod = "plain";
|
|
101
|
+
if (!codeChallengeMethod || !allowedCodeChallengeMethods.includes(codeChallengeMethod)) return handleRedirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method"));
|
|
102
|
+
query.code_challenge_method = codeChallengeMethod;
|
|
103
|
+
}
|
|
98
104
|
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
99
105
|
const codeExpiresInMs = opts.codeExpiresIn * 1e3;
|
|
100
106
|
const expiresAt = new Date(Date.now() + codeExpiresInMs);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mergeSchema } from "../../db/schema.mjs";
|
|
2
2
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
3
|
+
import { constantTimeEqual } from "../../crypto/buffer.mjs";
|
|
3
4
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
5
|
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
5
6
|
import { expireCookie } from "../../cookies/index.mjs";
|
|
@@ -52,11 +53,7 @@ const getMetadata = (ctx, options) => {
|
|
|
52
53
|
const jwtPlugin = ctx.context.getPlugin("jwt");
|
|
53
54
|
const issuer = jwtPlugin && jwtPlugin.options?.jwt && jwtPlugin.options.jwt.issuer ? jwtPlugin.options.jwt.issuer : ctx.context.options.baseURL;
|
|
54
55
|
const baseURL = ctx.context.baseURL;
|
|
55
|
-
const supportedAlgs = options?.useJWTPlugin ? [
|
|
56
|
-
"RS256",
|
|
57
|
-
"EdDSA",
|
|
58
|
-
"none"
|
|
59
|
-
] : ["HS256", "none"];
|
|
56
|
+
const supportedAlgs = options?.useJWTPlugin ? ["RS256", "EdDSA"] : ["HS256"];
|
|
60
57
|
return {
|
|
61
58
|
issuer,
|
|
62
59
|
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
@@ -161,7 +158,7 @@ const oidcProvider = (options) => {
|
|
|
161
158
|
defaultScope: "openid",
|
|
162
159
|
accessTokenExpiresIn: DEFAULT_ACCESS_TOKEN_EXPIRES_IN,
|
|
163
160
|
refreshTokenExpiresIn: DEFAULT_REFRESH_TOKEN_EXPIRES_IN,
|
|
164
|
-
allowPlainCodeChallengeMethod:
|
|
161
|
+
allowPlainCodeChallengeMethod: false,
|
|
165
162
|
storeClientSecret: "plain",
|
|
166
163
|
...options,
|
|
167
164
|
scopes: [
|
|
@@ -190,14 +187,14 @@ const oidcProvider = (options) => {
|
|
|
190
187
|
* Verify stored client secret against provided client secret
|
|
191
188
|
*/
|
|
192
189
|
async function verifyStoredClientSecret(ctx, storedClientSecret, clientSecret) {
|
|
193
|
-
if (opts.storeClientSecret === "encrypted") return await symmetricDecrypt({
|
|
190
|
+
if (opts.storeClientSecret === "encrypted") return constantTimeEqual(await symmetricDecrypt({
|
|
194
191
|
key: ctx.context.secretConfig,
|
|
195
192
|
data: storedClientSecret
|
|
196
|
-
})
|
|
197
|
-
if (opts.storeClientSecret === "hashed") return await defaultClientSecretHasher(clientSecret)
|
|
198
|
-
if (typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret) return await opts.storeClientSecret.hash(clientSecret)
|
|
199
|
-
if (typeof opts.storeClientSecret === "object" && "decrypt" in opts.storeClientSecret) return await opts.storeClientSecret.decrypt(storedClientSecret)
|
|
200
|
-
return clientSecret
|
|
193
|
+
}), clientSecret);
|
|
194
|
+
if (opts.storeClientSecret === "hashed") return constantTimeEqual(await defaultClientSecretHasher(clientSecret), storedClientSecret);
|
|
195
|
+
if (typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret) return constantTimeEqual(await opts.storeClientSecret.hash(clientSecret), storedClientSecret);
|
|
196
|
+
if (typeof opts.storeClientSecret === "object" && "decrypt" in opts.storeClientSecret) return constantTimeEqual(await opts.storeClientSecret.decrypt(storedClientSecret), clientSecret);
|
|
197
|
+
return constantTimeEqual(clientSecret, storedClientSecret);
|
|
201
198
|
}
|
|
202
199
|
return {
|
|
203
200
|
id: "oidc-provider",
|
|
@@ -378,25 +375,43 @@ const oidcProvider = (options) => {
|
|
|
378
375
|
});
|
|
379
376
|
let { client_id, client_secret } = body;
|
|
380
377
|
const authorization = ctx.request?.headers.get("authorization") || null;
|
|
381
|
-
if (authorization && !
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
378
|
+
if (authorization && !client_secret && authorization.startsWith("Basic ")) {
|
|
379
|
+
let decoded;
|
|
380
|
+
try {
|
|
381
|
+
const encoded = authorization.replace("Basic ", "");
|
|
382
|
+
decoded = new TextDecoder().decode(base64.decode(encoded));
|
|
383
|
+
} catch {
|
|
384
|
+
throw new APIError("UNAUTHORIZED", {
|
|
385
|
+
error_description: "invalid authorization header format",
|
|
386
|
+
error: "invalid_client"
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const colonIndex = decoded.indexOf(":");
|
|
390
|
+
if (colonIndex === -1) throw new APIError("UNAUTHORIZED", {
|
|
385
391
|
error_description: "invalid authorization header format",
|
|
386
392
|
error: "invalid_client"
|
|
387
393
|
});
|
|
388
|
-
|
|
394
|
+
let id;
|
|
395
|
+
let secret;
|
|
396
|
+
try {
|
|
397
|
+
id = decodeURIComponent(decoded.slice(0, colonIndex));
|
|
398
|
+
secret = decodeURIComponent(decoded.slice(colonIndex + 1));
|
|
399
|
+
} catch {
|
|
400
|
+
throw new APIError("UNAUTHORIZED", {
|
|
401
|
+
error_description: "invalid authorization header format",
|
|
402
|
+
error: "invalid_client"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
389
405
|
if (!id || !secret) throw new APIError("UNAUTHORIZED", {
|
|
390
406
|
error_description: "invalid authorization header format",
|
|
391
407
|
error: "invalid_client"
|
|
392
408
|
});
|
|
393
|
-
client_id
|
|
394
|
-
|
|
395
|
-
} catch {
|
|
396
|
-
throw new APIError("UNAUTHORIZED", {
|
|
397
|
-
error_description: "invalid authorization header format",
|
|
409
|
+
if (client_id && client_id.toString() !== id) throw new APIError("UNAUTHORIZED", {
|
|
410
|
+
error_description: "client_id in body does not match Authorization header",
|
|
398
411
|
error: "invalid_client"
|
|
399
412
|
});
|
|
413
|
+
client_id = id;
|
|
414
|
+
client_secret = secret;
|
|
400
415
|
}
|
|
401
416
|
const now = Date.now();
|
|
402
417
|
const iat = Math.floor(now / 1e3);
|
|
@@ -428,6 +443,25 @@ const oidcProvider = (options) => {
|
|
|
428
443
|
error_description: "refresh token expired",
|
|
429
444
|
error: "invalid_grant"
|
|
430
445
|
});
|
|
446
|
+
const refreshClient = await getClient(client_id.toString(), trustedClients);
|
|
447
|
+
if (!refreshClient) throw new APIError("UNAUTHORIZED", {
|
|
448
|
+
error_description: "invalid client_id",
|
|
449
|
+
error: "invalid_client"
|
|
450
|
+
});
|
|
451
|
+
if (refreshClient.disabled) throw new APIError("UNAUTHORIZED", {
|
|
452
|
+
error_description: "client is disabled",
|
|
453
|
+
error: "invalid_client"
|
|
454
|
+
});
|
|
455
|
+
if (refreshClient.type !== "public") {
|
|
456
|
+
if (!refreshClient.clientSecret || !client_secret) throw new APIError("UNAUTHORIZED", {
|
|
457
|
+
error_description: "client_secret is required for confidential clients",
|
|
458
|
+
error: "invalid_client"
|
|
459
|
+
});
|
|
460
|
+
if (!await verifyStoredClientSecret(ctx, refreshClient.clientSecret, client_secret.toString())) throw new APIError("UNAUTHORIZED", {
|
|
461
|
+
error_description: "invalid client_secret",
|
|
462
|
+
error: "invalid_client"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
431
465
|
const accessToken = generateRandomString(32, "a-z", "A-Z");
|
|
432
466
|
const newRefreshToken = generateRandomString(32, "a-z", "A-Z");
|
|
433
467
|
await ctx.context.adapter.create({
|
|
@@ -460,11 +494,7 @@ const oidcProvider = (options) => {
|
|
|
460
494
|
error_description: "code verifier is missing",
|
|
461
495
|
error: "invalid_request"
|
|
462
496
|
});
|
|
463
|
-
|
|
464
|
-
* We need to check if the code is valid before we can proceed
|
|
465
|
-
* with the rest of the request.
|
|
466
|
-
*/
|
|
467
|
-
const verificationValue = await ctx.context.internalAdapter.findVerificationValue(code.toString());
|
|
497
|
+
const verificationValue = await ctx.context.internalAdapter.consumeVerificationValue(code.toString());
|
|
468
498
|
if (!verificationValue) throw new APIError("UNAUTHORIZED", {
|
|
469
499
|
error_description: "invalid code",
|
|
470
500
|
error: "invalid_grant"
|
|
@@ -473,7 +503,6 @@ const oidcProvider = (options) => {
|
|
|
473
503
|
error_description: "code expired",
|
|
474
504
|
error: "invalid_grant"
|
|
475
505
|
});
|
|
476
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
|
|
477
506
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
478
507
|
error_description: "client_id is required",
|
|
479
508
|
error: "invalid_client"
|
|
@@ -527,12 +556,13 @@ const oidcProvider = (options) => {
|
|
|
527
556
|
error: "invalid_client"
|
|
528
557
|
});
|
|
529
558
|
}
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
559
|
+
if (value.codeChallenge) {
|
|
560
|
+
if ((value.codeChallengeMethod === "plain" ? code_verifier : await createHash("SHA-256", "base64urlnopad").digest(code_verifier)) !== value.codeChallenge) throw new APIError("UNAUTHORIZED", {
|
|
561
|
+
error_description: "code verification failed",
|
|
562
|
+
error: "invalid_request"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
534
565
|
const requestedScopes = value.scope;
|
|
535
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
|
|
536
566
|
const accessToken = generateRandomString(32, "a-z", "A-Z");
|
|
537
567
|
const refreshToken = generateRandomString(32, "A-Z", "a-z");
|
|
538
568
|
await ctx.context.adapter.create({
|
|
@@ -45,8 +45,9 @@ const oneTap = (options) => ({
|
|
|
45
45
|
} catch {
|
|
46
46
|
throw new APIError("BAD_REQUEST", { message: "invalid id token" });
|
|
47
47
|
}
|
|
48
|
-
const { email, email_verified, name, picture, sub } = payload;
|
|
49
|
-
if (!
|
|
48
|
+
const { email: rawEmail, email_verified, name, picture, sub } = payload;
|
|
49
|
+
if (!rawEmail) return ctx.json({ error: "Email not available in token" });
|
|
50
|
+
const email = rawEmail.toLowerCase();
|
|
50
51
|
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
51
52
|
if (!user) {
|
|
52
53
|
if (options?.disableSignup) throw new APIError("BAD_GATEWAY", { message: "User not found" });
|
|
@@ -70,14 +71,19 @@ const oneTap = (options) => ({
|
|
|
70
71
|
user: parseUserOutput(ctx.context.options, newUser.user)
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
|
-
if (!await ctx.context.internalAdapter.findAccount(sub))
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
if (!await ctx.context.internalAdapter.findAccount(sub)) {
|
|
75
|
+
const accountLinking = ctx.context.options.account?.accountLinking;
|
|
76
|
+
const providerEmailVerified = typeof email_verified === "boolean" ? email_verified : toBoolean(email_verified);
|
|
77
|
+
const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
|
|
78
|
+
if (accountLinking?.enabled !== false && accountLinking?.disableImplicitLinking !== true && (!requireLocalEmailVerified || user.user.emailVerified) && (ctx.context.trustedProviders.includes("google") || providerEmailVerified)) await ctx.context.internalAdapter.linkAccount({
|
|
79
|
+
userId: user.user.id,
|
|
80
|
+
providerId: "google",
|
|
81
|
+
accountId: sub,
|
|
82
|
+
scope: "openid,profile,email",
|
|
83
|
+
idToken
|
|
84
|
+
});
|
|
85
|
+
else throw new APIError("UNAUTHORIZED", { message: "Google identity cannot be linked: implicit account-linking is disabled, the local email is not verified, or the Google email_verified claim is false and Google is not a trusted provider" });
|
|
86
|
+
}
|
|
81
87
|
const session = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
82
88
|
await setSessionCookie(ctx, {
|
|
83
89
|
user: user.user,
|