better-auth 1.7.5 → 1.7.7
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/rate-limiter/index.mjs +4 -1
- package/dist/api/routes/callback.mjs +16 -64
- package/dist/api/routes/password.mjs +3 -5
- package/dist/api/routes/sign-in.mjs +4 -1
- package/dist/api/routes/sign-up.mjs +3 -10
- package/dist/api/routes/update-user.mjs +7 -20
- package/dist/client/config.mjs +3 -4
- package/dist/client/query-atom.mjs +13 -0
- package/dist/client/query.mjs +37 -27
- package/dist/client/react/react-store.mjs +2 -1
- package/dist/client/session-atom.mjs +2 -1
- package/dist/context/create-context.mjs +3 -2
- package/dist/crypto/purpose.mjs +19 -0
- package/dist/index.mjs +1 -0
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +42 -14
- package/dist/oauth2/link-account.mjs +61 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/admin.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +4 -10
- package/dist/plugins/admin/types.d.mts +4 -1
- package/dist/plugins/captcha/constants.d.mts +1 -0
- package/dist/plugins/captcha/constants.mjs +2 -1
- package/dist/plugins/captcha/index.d.mts +2 -2
- package/dist/plugins/captcha/index.mjs +6 -0
- package/dist/plugins/captcha/types.d.mts +44 -2
- package/dist/plugins/captcha/utils.mjs +11 -1
- package/dist/plugins/captcha/verify-handlers/vercel-botid.mjs +27 -0
- package/dist/plugins/email-otp/routes.mjs +3 -4
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/magic-link/index.mjs +23 -4
- package/dist/plugins/oauth-proxy/index.d.mts +5 -6
- package/dist/plugins/oauth-proxy/index.mjs +30 -15
- package/dist/plugins/open-api/index.mjs +1 -0
- package/dist/plugins/organization/client.mjs +2 -1
- package/dist/plugins/phone-number/routes.mjs +5 -4
- package/dist/plugins/username/index.mjs +2 -0
- package/dist/state.mjs +9 -6
- package/dist/utils/hashing.mjs +1 -1
- package/dist/utils/index.mjs +5 -0
- package/dist/utils/middleware-response.mjs +4 -1
- package/dist/utils/password.mjs +16 -2
- package/package.json +9 -8
|
@@ -42,8 +42,10 @@ const admin = (options) => {
|
|
|
42
42
|
});
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
+
const bannedUserMessage = opts.bannedUserMessage;
|
|
46
|
+
const message = typeof bannedUserMessage === "function" ? await bannedUserMessage(user) : bannedUserMessage;
|
|
45
47
|
throw APIError.from("FORBIDDEN", {
|
|
46
|
-
message
|
|
48
|
+
message,
|
|
47
49
|
code: "BANNED_USER"
|
|
48
50
|
});
|
|
49
51
|
}
|
|
@@ -2,6 +2,7 @@ import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
|
|
|
2
2
|
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
4
4
|
import { getAuthoritativeSessionFromCtx, getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
5
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
5
6
|
import { ADMIN_ERROR_CODES } from "./error-codes.mjs";
|
|
6
7
|
import { hasPermission } from "./has-permission.mjs";
|
|
7
8
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
@@ -190,6 +191,7 @@ const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
|
190
191
|
}
|
|
191
192
|
const email = ctx.body.email.toLowerCase();
|
|
192
193
|
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
|
|
194
|
+
if (ctx.body.password) assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
193
195
|
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw APIError.from("BAD_REQUEST", ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
194
196
|
const user = await ctx.context.internalAdapter.createUser({
|
|
195
197
|
...userData,
|
|
@@ -823,16 +825,8 @@ const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password",
|
|
|
823
825
|
permissions: { user: ["set-password"] }
|
|
824
826
|
})) throw APIError.from("FORBIDDEN", ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD);
|
|
825
827
|
const { newPassword, userId } = ctx.body;
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
ctx.context.logger.warn("Password is too short");
|
|
829
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
830
|
-
}
|
|
831
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
832
|
-
if (newPassword.length > maxPasswordLength) {
|
|
833
|
-
ctx.context.logger.warn("Password is too long");
|
|
834
|
-
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
835
|
-
}
|
|
828
|
+
assertPasswordNotTooShort(ctx, newPassword);
|
|
829
|
+
assertPasswordNotTooLong(ctx, newPassword);
|
|
836
830
|
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
837
831
|
if (!user) throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
838
832
|
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
@@ -2,6 +2,7 @@ import { Session, User } from "../../types/models.mjs";
|
|
|
2
2
|
import { InferOptionSchema } from "../../types/plugins.mjs";
|
|
3
3
|
import { AccessControl, Role } from "../access/types.mjs";
|
|
4
4
|
import { AdminSchema } from "./schema.mjs";
|
|
5
|
+
import { Awaitable } from "@better-auth/core";
|
|
5
6
|
|
|
6
7
|
//#region src/plugins/admin/types.d.ts
|
|
7
8
|
interface UserWithRole extends User {
|
|
@@ -70,8 +71,10 @@ interface AdminOptions {
|
|
|
70
71
|
* Message to show when a user is banned
|
|
71
72
|
*
|
|
72
73
|
* By default, the message is "You have been banned from this application"
|
|
74
|
+
*
|
|
75
|
+
* Can also be a function that receives the banned user and returns a message.
|
|
73
76
|
*/
|
|
74
|
-
bannedUserMessage?: string | undefined;
|
|
77
|
+
bannedUserMessage?: string | ((user: UserWithRole & Record<string, unknown>) => Awaitable<string>) | undefined;
|
|
75
78
|
/**
|
|
76
79
|
* Whether to allow impersonating other admins.
|
|
77
80
|
*
|
|
@@ -15,7 +15,8 @@ const Providers = {
|
|
|
15
15
|
CLOUDFLARE_TURNSTILE: "cloudflare-turnstile",
|
|
16
16
|
GOOGLE_RECAPTCHA: "google-recaptcha",
|
|
17
17
|
HCAPTCHA: "hcaptcha",
|
|
18
|
-
CAPTCHAFOX: "captchafox"
|
|
18
|
+
CAPTCHAFOX: "captchafox",
|
|
19
|
+
VERCEL_BOTID: "vercel-botid"
|
|
19
20
|
};
|
|
20
21
|
const siteVerifyMap = {
|
|
21
22
|
[Providers.CLOUDFLARE_TURNSTILE]: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider } from "./types.mjs";
|
|
1
|
+
import { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions } from "./types.mjs";
|
|
2
2
|
import * as _$_better_auth_core0 from "@better-auth/core";
|
|
3
3
|
import * as _$_better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
|
|
4
4
|
|
|
@@ -24,4 +24,4 @@ declare const captcha: (options: CaptchaOptions) => {
|
|
|
24
24
|
options: CaptchaOptions;
|
|
25
25
|
};
|
|
26
26
|
//#endregion
|
|
27
|
-
export { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, captcha };
|
|
27
|
+
export { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions, captcha };
|
|
@@ -7,6 +7,7 @@ import { captchaFox } from "./verify-handlers/captchafox.mjs";
|
|
|
7
7
|
import { cloudflareTurnstile } from "./verify-handlers/cloudflare-turnstile.mjs";
|
|
8
8
|
import { googleRecaptcha } from "./verify-handlers/google-recaptcha.mjs";
|
|
9
9
|
import { hCaptcha } from "./verify-handlers/h-captcha.mjs";
|
|
10
|
+
import { vercelBotId } from "./verify-handlers/vercel-botid.mjs";
|
|
10
11
|
import { getIP } from "@better-auth/core/utils/ip";
|
|
11
12
|
//#region src/plugins/captcha/index.ts
|
|
12
13
|
const normalizeEndpointPath = (pathname, basePath) => {
|
|
@@ -26,6 +27,11 @@ const captcha = (options) => ({
|
|
|
26
27
|
const basePath = ctx.options.basePath ?? "/api/auth";
|
|
27
28
|
const pathname = normalizeEndpointPath(url.pathname, basePath);
|
|
28
29
|
if (!endpoints.some((endpoint) => endpoint.includes("*") ? wildcardMatch(endpoint)(pathname) : endpoint === pathname)) return;
|
|
30
|
+
if (options.provider === Providers.VERCEL_BOTID) return await vercelBotId({
|
|
31
|
+
request,
|
|
32
|
+
checkBotId: options.checkBotId,
|
|
33
|
+
validateRequest: options.validateRequest
|
|
34
|
+
});
|
|
29
35
|
if (!options.secretKey) throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
|
|
30
36
|
const captchaResponse = request.headers.get("x-captcha-response");
|
|
31
37
|
const remoteUserIP = getIP(request, ctx.options) ?? void 0;
|
|
@@ -47,6 +47,48 @@ interface CaptchaFoxOptions extends BaseCaptchaOptions {
|
|
|
47
47
|
provider: typeof Providers.CAPTCHAFOX;
|
|
48
48
|
siteKey?: string | undefined;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* BotID verdict fields available to custom request validation.
|
|
52
|
+
*
|
|
53
|
+
* @see https://vercel.com/docs/botid/verified-bots
|
|
54
|
+
*/
|
|
55
|
+
type BotIdVerification = {
|
|
56
|
+
isBot: boolean;
|
|
57
|
+
isVerifiedBot?: boolean | undefined;
|
|
58
|
+
verifiedBotName?: string | undefined;
|
|
59
|
+
verifiedBotCategory?: string | undefined;
|
|
60
|
+
};
|
|
61
|
+
type ValidateRequestContext = {
|
|
62
|
+
request: Request;
|
|
63
|
+
verification: BotIdVerification;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Protect auth requests with Vercel BotID. The browser must also protect the
|
|
67
|
+
* same request paths and methods with BotID's client SDK.
|
|
68
|
+
*
|
|
69
|
+
* @see https://vercel.com/docs/botid/get-started
|
|
70
|
+
*/
|
|
71
|
+
interface VercelBotIdOptions {
|
|
72
|
+
provider: typeof Providers.VERCEL_BOTID;
|
|
73
|
+
/**
|
|
74
|
+
* Auth paths to verify, without the Better Auth base path.
|
|
75
|
+
*
|
|
76
|
+
* @default ["/sign-up/email", "/sign-in/email", "/request-password-reset"]
|
|
77
|
+
*/
|
|
78
|
+
endpoints?: string[] | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Vercel's server-side check for the current request. Pass `checkBotId` from
|
|
81
|
+
* `botid/server`, or wrap it to supply SDK options.
|
|
82
|
+
*/
|
|
83
|
+
checkBotId: () => Promise<BotIdVerification>;
|
|
84
|
+
/**
|
|
85
|
+
* Override the default `isBot === false` decision. Return `true` to allow
|
|
86
|
+
* the request, including a verified bot you trust.
|
|
87
|
+
*
|
|
88
|
+
* @see https://vercel.com/docs/botid/verified-bots
|
|
89
|
+
*/
|
|
90
|
+
validateRequest?: (ctx: ValidateRequestContext) => boolean | Promise<boolean>;
|
|
91
|
+
}
|
|
92
|
+
type CaptchaOptions = GoogleRecaptchaOptions | CloudflareTurnstileOptions | HCaptchaOptions | CaptchaFoxOptions | VercelBotIdOptions;
|
|
51
93
|
//#endregion
|
|
52
|
-
export { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider };
|
|
94
|
+
export { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions };
|
|
@@ -5,5 +5,15 @@ const encodeToURLParams = (obj) => {
|
|
|
5
5
|
for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) params.append(key, String(value));
|
|
6
6
|
return params.toString();
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Rejects after `timeoutMs` without cancelling the underlying promise.
|
|
10
|
+
*/
|
|
11
|
+
const withTimeout = (promise, timeoutMs) => {
|
|
12
|
+
let timer;
|
|
13
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
14
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Operation timed out")), timeoutMs);
|
|
15
|
+
});
|
|
16
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
17
|
+
};
|
|
8
18
|
//#endregion
|
|
9
|
-
export { encodeToURLParams };
|
|
19
|
+
export { encodeToURLParams, withTimeout };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { middlewareResponse } from "../../../utils/middleware-response.mjs";
|
|
2
|
+
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants.mjs";
|
|
3
|
+
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes.mjs";
|
|
4
|
+
import { withTimeout } from "../utils.mjs";
|
|
5
|
+
//#region src/plugins/captcha/verify-handlers/vercel-botid.ts
|
|
6
|
+
const vercelBotId = async ({ request, checkBotId, validateRequest }) => {
|
|
7
|
+
let isValid;
|
|
8
|
+
try {
|
|
9
|
+
isValid = await withTimeout((async () => {
|
|
10
|
+
const verification = await checkBotId();
|
|
11
|
+
return validateRequest ? validateRequest({
|
|
12
|
+
request,
|
|
13
|
+
verification
|
|
14
|
+
}) : verification.isBot === false;
|
|
15
|
+
})(), CAPTCHA_VERIFY_TIMEOUT_MS);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message, { cause: error });
|
|
18
|
+
}
|
|
19
|
+
if (isValid) return void 0;
|
|
20
|
+
return middlewareResponse({
|
|
21
|
+
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
|
22
|
+
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
|
23
|
+
status: 403
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { vercelBotId };
|
|
@@ -6,6 +6,7 @@ import { symmetricDecrypt } from "../../crypto/index.mjs";
|
|
|
6
6
|
import { setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
|
|
7
7
|
import { revokeUnprovenAccountAccess } from "../../db/revoke-unproven-account-access.mjs";
|
|
8
8
|
import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
|
|
9
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
9
10
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
10
11
|
import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
|
|
11
12
|
import { splitAtLastColon, toOTPIdentifier } from "./utils.mjs";
|
|
@@ -581,10 +582,8 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
|
|
|
581
582
|
} }
|
|
582
583
|
}, async (ctx) => {
|
|
583
584
|
const email = ctx.body.email.toLowerCase();
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
587
|
-
if (ctx.body.password.length > maxPasswordLength) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
585
|
+
assertPasswordNotTooShort(ctx, ctx.body.password);
|
|
586
|
+
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
588
587
|
await atomicVerifyOTP(ctx, opts, toOTPIdentifier("forget-password", email), ctx.body.otp);
|
|
589
588
|
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
590
589
|
if (!user) throw APIError$1.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ import { admin } from "./admin/admin.mjs";
|
|
|
10
10
|
import { AnonymousOptions, AnonymousSession, UserWithAnonymous } from "./anonymous/types.mjs";
|
|
11
11
|
import { anonymous } from "./anonymous/index.mjs";
|
|
12
12
|
import { BearerOptions, bearer } from "./bearer/index.mjs";
|
|
13
|
-
import { BaseCaptchaOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider } from "./captcha/types.mjs";
|
|
13
|
+
import { BaseCaptchaOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, GoogleRecaptchaOptions, HCaptchaOptions, Provider, ValidateRequestContext, VercelBotIdOptions } from "./captcha/types.mjs";
|
|
14
14
|
import { captcha } from "./captcha/index.mjs";
|
|
15
15
|
import { CustomSessionPluginOptions, customSession } from "./custom-session/index.mjs";
|
|
16
16
|
import { TimeString, ms, sec } from "../utils/time.mjs";
|
|
@@ -66,4 +66,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
|
|
|
66
66
|
import { UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, username } from "./username/index.mjs";
|
|
67
67
|
import { hasPermission } from "./organization/has-permission.mjs";
|
|
68
68
|
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
|
|
69
|
-
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
|
|
69
|
+
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, BotIdVerification, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, ValidateRequestContext, VercelBotIdOptions, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
|
|
@@ -23,6 +23,20 @@ const magicLinkVerifyQuerySchema = z.object({
|
|
|
23
23
|
errorCallbackURL: z.string().meta({ description: "URL to redirect after error." }).optional(),
|
|
24
24
|
newUserCallbackURL: z.string().meta({ description: "URL to redirect after new user signup. Only used if the user is registering for the first time." }).optional()
|
|
25
25
|
});
|
|
26
|
+
const magicLinkRecordSchema = z.object({
|
|
27
|
+
type: z.literal("magic-link"),
|
|
28
|
+
email: z.email(),
|
|
29
|
+
name: z.string().optional()
|
|
30
|
+
}).strict();
|
|
31
|
+
function parseMagicLinkRecord(value) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(value);
|
|
34
|
+
return magicLinkRecordSchema.safeParse(parsed);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const magicLinkIdentifier = (storedToken) => `magic-link:${storedToken}`;
|
|
26
40
|
const magicLink = (options) => {
|
|
27
41
|
const opts = {
|
|
28
42
|
storeToken: "plain",
|
|
@@ -75,8 +89,9 @@ const magicLink = (options) => {
|
|
|
75
89
|
const verificationToken = opts?.generateToken ? await opts.generateToken(email) : generateRandomString(32, "a-z", "A-Z");
|
|
76
90
|
const storedToken = await storeToken(ctx, verificationToken);
|
|
77
91
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
78
|
-
identifier: storedToken,
|
|
92
|
+
identifier: magicLinkIdentifier(storedToken),
|
|
79
93
|
value: JSON.stringify({
|
|
94
|
+
type: "magic-link",
|
|
80
95
|
email,
|
|
81
96
|
name: ctx.body.name
|
|
82
97
|
}),
|
|
@@ -152,10 +167,14 @@ const magicLink = (options) => {
|
|
|
152
167
|
throw ctx.redirect(errorCallbackURL.toString());
|
|
153
168
|
}
|
|
154
169
|
const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : callbackURL, ctx.context.baseURL).toString();
|
|
155
|
-
const
|
|
156
|
-
const
|
|
170
|
+
const identifier = magicLinkIdentifier(await storeToken(ctx, token));
|
|
171
|
+
const pendingValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
172
|
+
if (!pendingValue || !parseMagicLinkRecord(pendingValue.value)?.success) redirectWithError("INVALID_TOKEN");
|
|
173
|
+
const tokenValue = await ctx.context.internalAdapter.consumeVerificationValue(identifier);
|
|
157
174
|
if (!tokenValue) redirectWithError("INVALID_TOKEN");
|
|
158
|
-
const
|
|
175
|
+
const record = parseMagicLinkRecord(tokenValue.value);
|
|
176
|
+
if (!record?.success) redirectWithError("INVALID_TOKEN");
|
|
177
|
+
const { email, name } = record.data;
|
|
159
178
|
let isNewUser = false;
|
|
160
179
|
let user = await ctx.context.internalAdapter.findUserByEmail(email).then((res) => res?.user);
|
|
161
180
|
if (!user) if (!opts.disableSignUp) {
|
|
@@ -41,12 +41,11 @@ interface OAuthProxyOptions {
|
|
|
41
41
|
* A dedicated secret used to encrypt and decrypt data passed between
|
|
42
42
|
* servers during the OAuth proxy flow.
|
|
43
43
|
*
|
|
44
|
-
* When set, this secret
|
|
45
|
-
* `BETTER_AUTH_SECRET` for
|
|
46
|
-
* This limits
|
|
47
|
-
* environments (production, preview,
|
|
48
|
-
*
|
|
49
|
-
* protected by the main secret.
|
|
44
|
+
* When set, keys derived from this secret are used **instead of** keys
|
|
45
|
+
* derived from `BETTER_AUTH_SECRET` for OAuth proxy encryption.
|
|
46
|
+
* This limits exposure of data protected only by the main secret if
|
|
47
|
+
* the proxy secret is shared across environments (production, preview,
|
|
48
|
+
* development). A compromised proxy secret can still affect OAuth flows.
|
|
50
49
|
*
|
|
51
50
|
* All environments participating in the OAuth proxy flow must share
|
|
52
51
|
* the same `secret` value.
|
|
@@ -7,9 +7,10 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
7
7
|
import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
8
8
|
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
9
9
|
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
10
|
-
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
10
|
+
import { handleOAuthUserInfo, linkOAuthAccount } from "../../oauth2/link-account.mjs";
|
|
11
11
|
import { setOAuthState } from "../../api/state/oauth.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { derivePurposeKey } from "../../crypto/purpose.mjs";
|
|
13
|
+
import { getAuthStateVerificationIdentifier, parseGenericState } from "../../state.mjs";
|
|
13
14
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
14
15
|
import { parseJSON } from "../../client/parser.mjs";
|
|
15
16
|
import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
|
|
@@ -37,6 +38,7 @@ const passthroughPayloadSchema = z.looseObject({
|
|
|
37
38
|
updatedAt: true
|
|
38
39
|
}).shape),
|
|
39
40
|
profile: z.record(z.string(), z.unknown()).optional(),
|
|
41
|
+
scopes: z.array(z.string()).optional(),
|
|
40
42
|
state: z.string().min(1),
|
|
41
43
|
callbackURL: z.string().min(1),
|
|
42
44
|
newUserURL: z.string().optional(),
|
|
@@ -65,7 +67,7 @@ const oauthCallbackQuerySchema = z.object({
|
|
|
65
67
|
});
|
|
66
68
|
const oAuthProxy = (opts) => {
|
|
67
69
|
const maxAge = opts?.maxAge ?? 60;
|
|
68
|
-
const getEncryptionKey = (ctx) => opts?.secret ?? ctx.context.secretConfig;
|
|
70
|
+
const getEncryptionKey = (ctx, purpose) => derivePurposeKey(opts?.secret ?? ctx.context.secretConfig, purpose);
|
|
69
71
|
const oauthProxyCompletion = createAuthEndpoint("/callback/:id/oauth-proxy", {
|
|
70
72
|
method: "GET",
|
|
71
73
|
operationId: "oauthProxyCompletion",
|
|
@@ -83,7 +85,7 @@ const oAuthProxy = (opts) => {
|
|
|
83
85
|
let decryptedPayload;
|
|
84
86
|
try {
|
|
85
87
|
decryptedPayload = await symmetricDecrypt({
|
|
86
|
-
key: getEncryptionKey(ctx),
|
|
88
|
+
key: getEncryptionKey(ctx, "oauth-proxy-profile"),
|
|
87
89
|
data: encryptedProfile
|
|
88
90
|
});
|
|
89
91
|
} catch (e) {
|
|
@@ -107,7 +109,19 @@ const oAuthProxy = (opts) => {
|
|
|
107
109
|
ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
|
|
108
110
|
throw redirectOnError(ctx, errorURL, "payload_expired");
|
|
109
111
|
}
|
|
110
|
-
|
|
112
|
+
const stateData = await restoreOAuthProxyState(ctx, payload.state);
|
|
113
|
+
if (!stateData) throw redirectOnError(ctx, errorURL, "state_mismatch");
|
|
114
|
+
if (stateData.link) {
|
|
115
|
+
const linkResult = await linkOAuthAccount(ctx, {
|
|
116
|
+
link: stateData.link,
|
|
117
|
+
userInfo: payload.userInfo,
|
|
118
|
+
account: payload.account,
|
|
119
|
+
profile: payload.profile ?? {},
|
|
120
|
+
scopes: payload.scopes ?? payload.account.scope?.split(",")
|
|
121
|
+
});
|
|
122
|
+
if (!linkResult.linked) throw redirectOnError(ctx, errorURL, linkResult.error.code, linkResult.error.message);
|
|
123
|
+
throw ctx.redirect(payload.callbackURL);
|
|
124
|
+
}
|
|
111
125
|
let result;
|
|
112
126
|
try {
|
|
113
127
|
result = await handleOAuthUserInfo(ctx, {
|
|
@@ -185,7 +199,7 @@ const oAuthProxy = (opts) => {
|
|
|
185
199
|
hooks: {
|
|
186
200
|
before: [{
|
|
187
201
|
matcher(context) {
|
|
188
|
-
return !!context.path?.startsWith("/sign-in/social");
|
|
202
|
+
return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
|
|
189
203
|
},
|
|
190
204
|
handler: createAuthMiddleware(async (ctx) => {
|
|
191
205
|
if (checkSkipProxy(ctx, opts)) return;
|
|
@@ -212,7 +226,7 @@ const oAuthProxy = (opts) => {
|
|
|
212
226
|
let statePackage;
|
|
213
227
|
try {
|
|
214
228
|
statePackage = parseJSON(await symmetricDecrypt({
|
|
215
|
-
key: getEncryptionKey(ctx),
|
|
229
|
+
key: getEncryptionKey(ctx, "oauth-proxy-package"),
|
|
216
230
|
data: state
|
|
217
231
|
}));
|
|
218
232
|
} catch {
|
|
@@ -232,7 +246,7 @@ const oAuthProxy = (opts) => {
|
|
|
232
246
|
let stateData;
|
|
233
247
|
try {
|
|
234
248
|
stateData = parseJSON(await symmetricDecrypt({
|
|
235
|
-
key: getEncryptionKey(ctx),
|
|
249
|
+
key: getEncryptionKey(ctx, "oauth-proxy-state"),
|
|
236
250
|
data: statePackage.stateCookie
|
|
237
251
|
}));
|
|
238
252
|
} catch (e) {
|
|
@@ -307,6 +321,7 @@ const oAuthProxy = (opts) => {
|
|
|
307
321
|
emailVerified: userInfo.emailVerified
|
|
308
322
|
},
|
|
309
323
|
profile: providerProfile,
|
|
324
|
+
scopes: tokens.scopes,
|
|
310
325
|
account: {
|
|
311
326
|
...accountKey,
|
|
312
327
|
accessToken: tokens.accessToken,
|
|
@@ -324,7 +339,7 @@ const oAuthProxy = (opts) => {
|
|
|
324
339
|
timestamp: Date.now()
|
|
325
340
|
};
|
|
326
341
|
const encryptedPayload = await symmetricEncrypt({
|
|
327
|
-
key: getEncryptionKey(ctx),
|
|
342
|
+
key: getEncryptionKey(ctx, "oauth-proxy-profile"),
|
|
328
343
|
data: JSON.stringify(payload)
|
|
329
344
|
});
|
|
330
345
|
proxyCallbackURL.searchParams.set("profile", encryptedPayload);
|
|
@@ -333,14 +348,14 @@ const oAuthProxy = (opts) => {
|
|
|
333
348
|
}],
|
|
334
349
|
after: [{
|
|
335
350
|
matcher(context) {
|
|
336
|
-
return !!context.path?.startsWith("/sign-in/social");
|
|
351
|
+
return !!context.path?.startsWith("/sign-in/social") || context.path === "/link-social";
|
|
337
352
|
},
|
|
338
353
|
handler: createAuthMiddleware(async (ctx) => {
|
|
339
354
|
if (checkSkipProxy(ctx, opts)) return;
|
|
340
355
|
const signInResponse = ctx.context.returned;
|
|
341
356
|
if (!signInResponse || typeof signInResponse !== "object" || !("url" in signInResponse)) return;
|
|
342
357
|
const { url: providerURL } = signInResponse;
|
|
343
|
-
if (typeof providerURL !== "string") return;
|
|
358
|
+
if (typeof providerURL !== "string" || providerURL.length === 0) return;
|
|
344
359
|
const oauthURL = new URL(providerURL);
|
|
345
360
|
const originalState = oauthURL.searchParams.get("state");
|
|
346
361
|
if (!originalState) return;
|
|
@@ -352,11 +367,11 @@ const oAuthProxy = (opts) => {
|
|
|
352
367
|
const oauthStateCookie = ctx.context.createAuthCookie("oauth_state");
|
|
353
368
|
const encryptedCookieValue = parseSetCookieHeader(setCookieHeader).get(oauthStateCookie.name)?.value;
|
|
354
369
|
if (encryptedCookieValue) plaintextState = await symmetricDecrypt({
|
|
355
|
-
key: ctx.context.secretConfig,
|
|
370
|
+
key: derivePurposeKey(ctx.context.secretConfig, "oauth-state-cookie"),
|
|
356
371
|
data: encryptedCookieValue
|
|
357
372
|
});
|
|
358
373
|
}
|
|
359
|
-
} else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(originalState))?.value;
|
|
374
|
+
} else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(getAuthStateVerificationIdentifier(originalState)))?.value;
|
|
360
375
|
if (!plaintextState) {
|
|
361
376
|
ctx.context.logger.warn("No OAuth state found for proxy");
|
|
362
377
|
return;
|
|
@@ -364,13 +379,13 @@ const oAuthProxy = (opts) => {
|
|
|
364
379
|
const statePackage = {
|
|
365
380
|
state: originalState,
|
|
366
381
|
stateCookie: await symmetricEncrypt({
|
|
367
|
-
key: getEncryptionKey(ctx),
|
|
382
|
+
key: getEncryptionKey(ctx, "oauth-proxy-state"),
|
|
368
383
|
data: plaintextState
|
|
369
384
|
}),
|
|
370
385
|
isOAuthProxy: true
|
|
371
386
|
};
|
|
372
387
|
const encryptedPackage = await symmetricEncrypt({
|
|
373
|
-
key: getEncryptionKey(ctx),
|
|
388
|
+
key: getEncryptionKey(ctx, "oauth-proxy-package"),
|
|
374
389
|
data: JSON.stringify(statePackage)
|
|
375
390
|
});
|
|
376
391
|
oauthURL.searchParams.set("state", encryptedPackage);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
2
2
|
import { useAuthQuery } from "../../client/query.mjs";
|
|
3
|
+
import { matchesSessionSignal } from "../../client/config.mjs";
|
|
3
4
|
import { adminAc, defaultRoles, memberAc, ownerAc } from "./access/statement.mjs";
|
|
4
5
|
import { ORGANIZATION_ERROR_CODES } from "./error-codes.mjs";
|
|
5
6
|
import { hasPermissionFn } from "./permission.mjs";
|
|
@@ -71,7 +72,7 @@ const organizationClient = (options) => {
|
|
|
71
72
|
},
|
|
72
73
|
{
|
|
73
74
|
matcher(path) {
|
|
74
|
-
return path
|
|
75
|
+
return matchesSessionSignal(path) || path.startsWith("/organization");
|
|
75
76
|
},
|
|
76
77
|
signal: "$activeOrgSignal"
|
|
77
78
|
},
|
|
@@ -4,6 +4,8 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
8
|
+
import "../../utils/index.mjs";
|
|
7
9
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
8
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -54,6 +56,7 @@ const signInPhoneNumber = (opts) => createAuthEndpoint("/sign-in/phone-number",
|
|
|
54
56
|
if (opts.phoneNumberValidator) {
|
|
55
57
|
if (!await opts.phoneNumberValidator(ctx.body.phoneNumber)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_PHONE_NUMBER);
|
|
56
58
|
}
|
|
59
|
+
assertPasswordNotTooLong(ctx, password);
|
|
57
60
|
const user = await ctx.context.adapter.findOne({
|
|
58
61
|
model: "user",
|
|
59
62
|
where: [{
|
|
@@ -466,10 +469,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
466
469
|
}]
|
|
467
470
|
});
|
|
468
471
|
if (!user) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (ctx.body.newPassword.length < minLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
|
|
472
|
-
if (ctx.body.newPassword.length > maxLength) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
|
|
472
|
+
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
473
|
+
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
473
474
|
const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
|
|
474
475
|
if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
|
|
475
476
|
userId: user.id,
|
|
@@ -2,6 +2,7 @@ import { mergeSchema, parseUserOutput } from "../../db/schema.mjs";
|
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
3
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
4
4
|
import { createEmailVerificationToken } from "../../api/routes/email-verification.mjs";
|
|
5
|
+
import { assertPasswordNotTooLong } from "../../utils/password.mjs";
|
|
5
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
6
7
|
import { USERNAME_ERROR_CODES } from "./error-codes.mjs";
|
|
7
8
|
import { getSchema } from "./schema.mjs";
|
|
@@ -166,6 +167,7 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
166
167
|
throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
|
|
167
168
|
}
|
|
168
169
|
if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
|
|
170
|
+
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
169
171
|
const user = await ctx.context.adapter.findOne({
|
|
170
172
|
model: "user",
|
|
171
173
|
where: [{
|
package/dist/state.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateRandomString } from "./crypto/random.mjs";
|
|
2
2
|
import { symmetricDecrypt, symmetricEncrypt } from "./crypto/index.mjs";
|
|
3
3
|
import { expireCookie } from "./cookies/index.mjs";
|
|
4
|
+
import { derivePurposeKey } from "./crypto/purpose.mjs";
|
|
4
5
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
6
|
import * as z from "zod";
|
|
6
7
|
//#region src/state.ts
|
|
@@ -32,6 +33,8 @@ const stateDataSchema = z.looseObject({
|
|
|
32
33
|
*/
|
|
33
34
|
serverContext: z.record(z.string(), z.unknown()).optional()
|
|
34
35
|
});
|
|
36
|
+
/** Verification key for database-backed OAuth and SAML relay state. */
|
|
37
|
+
const getAuthStateVerificationIdentifier = (state) => `auth-state:${state}`;
|
|
35
38
|
const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape));
|
|
36
39
|
var StateError = class extends BetterAuthError {
|
|
37
40
|
code;
|
|
@@ -59,7 +62,7 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
59
62
|
oauthState: state
|
|
60
63
|
};
|
|
61
64
|
const encryptedData = await symmetricEncrypt({
|
|
62
|
-
key: c.context.secretConfig,
|
|
65
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-state-cookie"),
|
|
63
66
|
data: JSON.stringify(payload)
|
|
64
67
|
});
|
|
65
68
|
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state", { maxAge: 600 });
|
|
@@ -78,7 +81,7 @@ async function generateGenericState(c, stateData, settings) {
|
|
|
78
81
|
...stateData,
|
|
79
82
|
oauthState: state
|
|
80
83
|
}),
|
|
81
|
-
identifier: state,
|
|
84
|
+
identifier: getAuthStateVerificationIdentifier(state),
|
|
82
85
|
expiresAt
|
|
83
86
|
})) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" });
|
|
84
87
|
return {
|
|
@@ -99,7 +102,7 @@ async function parseGenericState(c, state, settings) {
|
|
|
99
102
|
});
|
|
100
103
|
try {
|
|
101
104
|
const decryptedData = await symmetricDecrypt({
|
|
102
|
-
key: c.context.secretConfig,
|
|
105
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-state-cookie"),
|
|
103
106
|
data: encryptedData
|
|
104
107
|
});
|
|
105
108
|
parsedData = stateDataSchema.parse(JSON.parse(decryptedData));
|
|
@@ -117,7 +120,7 @@ async function parseGenericState(c, state, settings) {
|
|
|
117
120
|
});
|
|
118
121
|
expireCookie(c, stateCookie);
|
|
119
122
|
} else {
|
|
120
|
-
const data = await c.context.internalAdapter.findVerificationValue(state);
|
|
123
|
+
const data = await c.context.internalAdapter.findVerificationValue(getAuthStateVerificationIdentifier(state));
|
|
121
124
|
if (!data) throw new StateError("State mismatch: verification not found", {
|
|
122
125
|
code: "state_mismatch",
|
|
123
126
|
details: { state }
|
|
@@ -136,7 +139,7 @@ async function parseGenericState(c, state, settings) {
|
|
|
136
139
|
errorURL: parsedData.errorURL
|
|
137
140
|
});
|
|
138
141
|
expireCookie(c, stateCookie);
|
|
139
|
-
await c.context.internalAdapter.deleteVerificationByIdentifier(state);
|
|
142
|
+
await c.context.internalAdapter.deleteVerificationByIdentifier(getAuthStateVerificationIdentifier(state));
|
|
140
143
|
}
|
|
141
144
|
if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", {
|
|
142
145
|
code: "state_mismatch",
|
|
@@ -146,4 +149,4 @@ async function parseGenericState(c, state, settings) {
|
|
|
146
149
|
return parsedData;
|
|
147
150
|
}
|
|
148
151
|
//#endregion
|
|
149
|
-
export { INTERNAL_STATE_KEYS, StateError, generateGenericState, parseGenericState };
|
|
152
|
+
export { INTERNAL_STATE_KEYS, StateError, generateGenericState, getAuthStateVerificationIdentifier, parseGenericState };
|
package/dist/utils/hashing.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2
1
|
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
2
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
3
3
|
//#region src/utils/hashing.ts
|
|
4
4
|
/**
|
|
5
5
|
* TS implementation of ERC-55 ("Mixed-case checksum address encoding") using @noble/hashes
|