better-auth 1.6.10 → 1.6.12
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 +8 -2
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +36 -40
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/session.mjs +14 -9
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +2 -1
- package/dist/api/routes/sign-up.d.mts +1 -0
- package/dist/api/routes/sign-up.mjs +9 -7
- package/dist/api/routes/update-user.mjs +5 -5
- package/dist/client/index.d.mts +2 -2
- package/dist/client/parser.mjs +0 -1
- package/dist/client/plugins/index.d.mts +3 -3
- package/dist/client/proxy.mjs +2 -1
- package/dist/context/helpers.mjs +3 -2
- package/dist/cookies/cookie-utils.d.mts +24 -1
- package/dist/cookies/cookie-utils.mjs +85 -22
- package/dist/cookies/index.d.mts +2 -3
- package/dist/cookies/index.mjs +39 -11
- package/dist/cookies/session-store.mjs +4 -23
- package/dist/db/get-migration.mjs +4 -4
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +3 -2
- package/dist/db/internal-adapter.mjs +96 -1
- package/dist/db/schema.d.mts +15 -2
- package/dist/db/schema.mjs +26 -1
- package/dist/db/with-hooks.d.mts +1 -0
- package/dist/db/with-hooks.mjs +58 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +16 -1
- package/dist/oauth2/link-account.mjs +6 -4
- package/dist/oauth2/state.mjs +8 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.d.mts +3 -15
- package/dist/plugins/access/access.mjs +11 -6
- 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/admin.mjs +0 -4
- package/dist/plugins/admin/client.d.mts +1 -1
- 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 +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- 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/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +6 -6
- package/dist/plugins/generic-oauth/routes.mjs +34 -32
- package/dist/plugins/generic-oauth/types.d.mts +7 -0
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/last-login-method/client.mjs +2 -2
- package/dist/plugins/magic-link/index.d.mts +8 -1
- package/dist/plugins/magic-link/index.mjs +4 -17
- package/dist/plugins/mcp/authorize.mjs +8 -2
- package/dist/plugins/mcp/index.mjs +73 -34
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +44 -31
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/authorize.mjs +8 -2
- package/dist/plugins/oidc-provider/index.mjs +63 -37
- package/dist/plugins/one-tap/index.mjs +13 -8
- package/dist/plugins/open-api/generator.mjs +16 -5
- package/dist/plugins/organization/access/statement.d.mts +68 -201
- package/dist/plugins/organization/adapter.mjs +61 -56
- package/dist/plugins/organization/client.d.mts +3 -1
- package/dist/plugins/organization/error-codes.d.mts +2 -0
- package/dist/plugins/organization/error-codes.mjs +3 -1
- package/dist/plugins/organization/routes/crud-access-control.d.mts +2 -2
- package/dist/plugins/organization/routes/crud-invites.mjs +7 -2
- package/dist/plugins/organization/types.d.mts +12 -2
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/plugins/username/index.d.mts +24 -2
- package/dist/plugins/username/index.mjs +49 -3
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +18 -4
- package/dist/test-utils/headers.mjs +2 -7
- package/dist/test-utils/test-instance.d.mts +25 -6
- package/dist/test-utils/test-instance.mjs +11 -2
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +9 -3
- package/package.json +15 -14
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
1
2
|
import { getOrigin } from "../../utils/url.mjs";
|
|
2
3
|
import { originCheck } from "../../api/middlewares/origin-check.mjs";
|
|
3
4
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
4
5
|
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
5
6
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
7
|
+
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
6
8
|
import { parseGenericState } from "../../state.mjs";
|
|
7
9
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
8
10
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
9
11
|
import { parseJSON } from "../../client/parser.mjs";
|
|
10
|
-
import { checkSkipProxy,
|
|
12
|
+
import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
|
|
11
13
|
import { defu } from "defu";
|
|
12
14
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
13
15
|
import * as z from "zod";
|
|
@@ -90,18 +92,28 @@ const oAuthProxy = (opts) => {
|
|
|
90
92
|
throw redirectOnError(ctx, errorURL, "payload_expired");
|
|
91
93
|
}
|
|
92
94
|
try {
|
|
93
|
-
await parseGenericState(ctx, payload.state);
|
|
95
|
+
await parseGenericState(ctx, payload.state, { skipStateCookieCheck: true });
|
|
94
96
|
} catch (e) {
|
|
95
97
|
ctx.context.logger.warn("Failed to clean up OAuth state", e);
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await handleOAuthUserInfo(ctx, {
|
|
102
|
+
userInfo: payload.userInfo,
|
|
103
|
+
account: payload.account,
|
|
104
|
+
callbackURL: payload.callbackURL,
|
|
105
|
+
disableSignUp: payload.disableSignUp
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
if (result.error) {
|
|
112
|
+
ctx.context.logger.error("OAuth proxy callback error", result.error);
|
|
113
|
+
throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
|
|
114
|
+
}
|
|
115
|
+
if (!result.data) {
|
|
116
|
+
ctx.context.logger.error("OAuth proxy callback missing session data");
|
|
105
117
|
throw redirectOnError(ctx, errorURL, "user_creation_failed");
|
|
106
118
|
}
|
|
107
119
|
await setSessionCookie(ctx, result.data);
|
|
@@ -141,6 +153,7 @@ const oAuthProxy = (opts) => {
|
|
|
141
153
|
data: state
|
|
142
154
|
}));
|
|
143
155
|
} catch {
|
|
156
|
+
ctx.context.logger.debug("OAuth proxy: could not decrypt state package, falling back to regular callback");
|
|
144
157
|
return;
|
|
145
158
|
}
|
|
146
159
|
if (!statePackage.isOAuthProxy || !statePackage.state || !statePackage.stateCookie) {
|
|
@@ -248,29 +261,29 @@ const oAuthProxy = (opts) => {
|
|
|
248
261
|
const oauthURL = new URL(providerURL);
|
|
249
262
|
const originalState = oauthURL.searchParams.get("state");
|
|
250
263
|
if (!originalState) return;
|
|
251
|
-
let stateCookieValue;
|
|
252
|
-
if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
|
|
253
|
-
const setCookieHeader = ctx.context.responseHeaders?.get("set-cookie");
|
|
254
|
-
if (setCookieHeader) {
|
|
255
|
-
const parsedCookies = parseSetCookieHeader(setCookieHeader);
|
|
256
|
-
const stateCookie = ctx.context.createAuthCookie("oauth_state");
|
|
257
|
-
stateCookieValue = parsedCookies.get(stateCookie.name)?.value;
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
const verification = await ctx.context.internalAdapter.findVerificationValue(originalState);
|
|
261
|
-
if (verification) stateCookieValue = await symmetricEncrypt({
|
|
262
|
-
key: getEncryptionKey(ctx),
|
|
263
|
-
data: verification.value
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (!stateCookieValue) {
|
|
267
|
-
ctx.context.logger.warn("No OAuth state cookie value found");
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
264
|
try {
|
|
265
|
+
let plaintextState;
|
|
266
|
+
if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
|
|
267
|
+
const setCookieHeader = ctx.context.responseHeaders?.get("set-cookie");
|
|
268
|
+
if (setCookieHeader) {
|
|
269
|
+
const oauthStateCookie = ctx.context.createAuthCookie("oauth_state");
|
|
270
|
+
const encryptedCookieValue = parseSetCookieHeader(setCookieHeader).get(oauthStateCookie.name)?.value;
|
|
271
|
+
if (encryptedCookieValue) plaintextState = await symmetricDecrypt({
|
|
272
|
+
key: ctx.context.secretConfig,
|
|
273
|
+
data: encryptedCookieValue
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
} else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(originalState))?.value;
|
|
277
|
+
if (!plaintextState) {
|
|
278
|
+
ctx.context.logger.warn("No OAuth state found for proxy");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
271
281
|
const statePackage = {
|
|
272
282
|
state: originalState,
|
|
273
|
-
stateCookie:
|
|
283
|
+
stateCookie: await symmetricEncrypt({
|
|
284
|
+
key: getEncryptionKey(ctx),
|
|
285
|
+
data: plaintextState
|
|
286
|
+
}),
|
|
274
287
|
isOAuthProxy: true
|
|
275
288
|
};
|
|
276
289
|
const encryptedPackage = await symmetricEncrypt({
|
|
@@ -283,7 +296,7 @@ const oAuthProxy = (opts) => {
|
|
|
283
296
|
url: oauthURL.toString()
|
|
284
297
|
};
|
|
285
298
|
} catch (e) {
|
|
286
|
-
ctx.context.logger.error("Failed to
|
|
299
|
+
ctx.context.logger.error("Failed to prepare OAuth proxy state:", e);
|
|
287
300
|
}
|
|
288
301
|
})
|
|
289
302
|
}, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOrigin } from "../../utils/url.mjs";
|
|
1
|
+
import { getOrigin, trimTrailingSlashes } from "../../utils/url.mjs";
|
|
2
2
|
import { env } from "@better-auth/core/env";
|
|
3
3
|
//#region src/plugins/oauth-proxy/utils.ts
|
|
4
4
|
/**
|
|
@@ -6,7 +6,7 @@ import { env } from "@better-auth/core/env";
|
|
|
6
6
|
*/
|
|
7
7
|
function stripTrailingSlash(url) {
|
|
8
8
|
if (!url) return "";
|
|
9
|
-
return url
|
|
9
|
+
return trimTrailingSlashes(url);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Get base URL from vendor-specific environment variables
|
|
@@ -37,12 +37,5 @@ function checkSkipProxy(ctx, opts) {
|
|
|
37
37
|
if (!currentURL) return false;
|
|
38
38
|
return getOrigin(productionURL) === getOrigin(currentURL);
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Redirect to error URL with error code
|
|
42
|
-
*/
|
|
43
|
-
function redirectOnError(ctx, errorURL, error) {
|
|
44
|
-
const sep = errorURL.includes("?") ? "&" : "?";
|
|
45
|
-
throw ctx.redirect(`${errorURL}${sep}error=${error}`);
|
|
46
|
-
}
|
|
47
40
|
//#endregion
|
|
48
|
-
export { checkSkipProxy,
|
|
41
|
+
export { checkSkipProxy, resolveCurrentURL, stripTrailingSlash };
|
|
@@ -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,20 +494,11 @@ 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"
|
|
471
501
|
});
|
|
472
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
473
|
-
error_description: "code expired",
|
|
474
|
-
error: "invalid_grant"
|
|
475
|
-
});
|
|
476
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
|
|
477
502
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
478
503
|
error_description: "client_id is required",
|
|
479
504
|
error: "invalid_client"
|
|
@@ -527,12 +552,13 @@ const oidcProvider = (options) => {
|
|
|
527
552
|
error: "invalid_client"
|
|
528
553
|
});
|
|
529
554
|
}
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
555
|
+
if (value.codeChallenge) {
|
|
556
|
+
if ((value.codeChallengeMethod === "plain" ? code_verifier : await createHash("SHA-256", "base64urlnopad").digest(code_verifier)) !== value.codeChallenge) throw new APIError("UNAUTHORIZED", {
|
|
557
|
+
error_description: "code verification failed",
|
|
558
|
+
error: "invalid_request"
|
|
559
|
+
});
|
|
560
|
+
}
|
|
534
561
|
const requestedScopes = value.scope;
|
|
535
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(code.toString());
|
|
536
562
|
const accessToken = generateRandomString(32, "a-z", "A-Z");
|
|
537
563
|
const refreshToken = generateRandomString(32, "A-Z", "a-z");
|
|
538
564
|
await ctx.context.adapter.create({
|
|
@@ -71,14 +71,19 @@ const oneTap = (options) => ({
|
|
|
71
71
|
user: parseUserOutput(ctx.context.options, newUser.user)
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
if (!await ctx.context.internalAdapter.findAccount(sub))
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
87
|
const session = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
83
88
|
await setSessionCookie(ctx, {
|
|
84
89
|
user: user.user,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db_exports } from "../../db/index.mjs";
|
|
2
2
|
import { getEndpoints } from "../../api/index.mjs";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { toPascalCase } from "@better-auth/core/utils/string";
|
|
4
5
|
//#region src/plugins/open-api/generator.ts
|
|
5
6
|
const allowedType = new Set([
|
|
6
7
|
"string",
|
|
@@ -156,7 +157,7 @@ function getResponse(responses) {
|
|
|
156
157
|
} } },
|
|
157
158
|
description: "Internal Server Error. This is a problem with the server that you cannot fix."
|
|
158
159
|
},
|
|
159
|
-
...responses
|
|
160
|
+
...responses ? structuredClone(responses) : {}
|
|
160
161
|
};
|
|
161
162
|
}
|
|
162
163
|
function toOpenApiPath(path) {
|
|
@@ -196,6 +197,16 @@ async function generator(ctx, options) {
|
|
|
196
197
|
return acc;
|
|
197
198
|
}, {}) } };
|
|
198
199
|
const paths = {};
|
|
200
|
+
const seenOperationIds = /* @__PURE__ */ new Set();
|
|
201
|
+
const uniqueOperationId = (operationId, method) => {
|
|
202
|
+
if (!operationId) return void 0;
|
|
203
|
+
const base = seenOperationIds.has(operationId) ? `${operationId}${toPascalCase(method)}` : operationId;
|
|
204
|
+
let result = base;
|
|
205
|
+
let n = 2;
|
|
206
|
+
while (seenOperationIds.has(result)) result = `${base}${n++}`;
|
|
207
|
+
seenOperationIds.add(result);
|
|
208
|
+
return result;
|
|
209
|
+
};
|
|
199
210
|
Object.entries(baseEndpoints.api).forEach(([_, value]) => {
|
|
200
211
|
if (!value.path || ctx.options.disabledPaths?.includes(value.path)) return;
|
|
201
212
|
const options = value.options;
|
|
@@ -207,7 +218,7 @@ async function generator(ctx, options) {
|
|
|
207
218
|
[method.toLowerCase()]: {
|
|
208
219
|
tags: ["Default", ...options.metadata?.openapi?.tags || []],
|
|
209
220
|
description: options.metadata?.openapi?.description,
|
|
210
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
221
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
211
222
|
security: [{ bearerAuth: [] }],
|
|
212
223
|
parameters: getParameters(options),
|
|
213
224
|
responses: getResponse(options.metadata?.openapi?.responses)
|
|
@@ -220,7 +231,7 @@ async function generator(ctx, options) {
|
|
|
220
231
|
[method.toLowerCase()]: {
|
|
221
232
|
tags: ["Default", ...options.metadata?.openapi?.tags || []],
|
|
222
233
|
description: options.metadata?.openapi?.description,
|
|
223
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
234
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
224
235
|
security: [{ bearerAuth: [] }],
|
|
225
236
|
parameters: getParameters(options),
|
|
226
237
|
...body ? { requestBody: body } : { requestBody: { content: { "application/json": { schema: {
|
|
@@ -253,7 +264,7 @@ async function generator(ctx, options) {
|
|
|
253
264
|
[method.toLowerCase()]: {
|
|
254
265
|
tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
|
|
255
266
|
description: options.metadata?.openapi?.description,
|
|
256
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
267
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
257
268
|
security: [{ bearerAuth: [] }],
|
|
258
269
|
parameters: getParameters(options),
|
|
259
270
|
responses: getResponse(options.metadata?.openapi?.responses)
|
|
@@ -264,7 +275,7 @@ async function generator(ctx, options) {
|
|
|
264
275
|
[method.toLowerCase()]: {
|
|
265
276
|
tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
|
|
266
277
|
description: options.metadata?.openapi?.description,
|
|
267
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
278
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
268
279
|
security: [{ bearerAuth: [] }],
|
|
269
280
|
parameters: getParameters(options),
|
|
270
281
|
requestBody: getRequestBody(options),
|