better-auth 1.6.21 → 1.6.23
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/client/plugins/index.d.mts +2 -1
- package/dist/db/index.d.mts +2 -1
- package/dist/db/index.mjs +3 -1
- package/dist/db/revoke-unproven-account-access.d.mts +19 -0
- package/dist/db/revoke-unproven-account-access.mjs +23 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/email-otp/routes.mjs +5 -1
- package/dist/plugins/generic-oauth/client.d.mts +1 -0
- package/dist/plugins/generic-oauth/index.d.mts +2 -1
- package/dist/plugins/generic-oauth/index.mjs +2 -1
- package/dist/plugins/generic-oauth/providers/index.d.mts +2 -1
- package/dist/plugins/generic-oauth/providers/yandex.d.mts +29 -0
- package/dist/plugins/generic-oauth/providers/yandex.mjs +60 -0
- package/dist/plugins/index.d.mts +2 -1
- package/dist/plugins/index.mjs +2 -1
- package/dist/plugins/magic-link/index.mjs +5 -1
- package/dist/plugins/two-factor/backup-codes/index.mjs +4 -1
- package/dist/plugins/two-factor/client.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.mjs +1 -0
- package/dist/plugins/two-factor/index.d.mts +14 -0
- package/dist/plugins/two-factor/otp/index.mjs +17 -1
- package/dist/plugins/two-factor/schema.d.mts +13 -0
- package/dist/plugins/two-factor/schema.mjs +13 -0
- package/dist/plugins/two-factor/totp/index.mjs +4 -1
- package/dist/plugins/two-factor/types.d.mts +25 -0
- package/dist/plugins/two-factor/verify-two-factor.mjs +83 -1
- package/package.json +8 -8
|
@@ -14,6 +14,7 @@ import { MicrosoftEntraIdOptions, microsoftEntraId } from "../../plugins/generic
|
|
|
14
14
|
import { OktaOptions, okta } from "../../plugins/generic-oauth/providers/okta.mjs";
|
|
15
15
|
import { PatreonOptions, patreon } from "../../plugins/generic-oauth/providers/patreon.mjs";
|
|
16
16
|
import { SlackOptions, slack } from "../../plugins/generic-oauth/providers/slack.mjs";
|
|
17
|
+
import { YandexOptions, yandex } from "../../plugins/generic-oauth/providers/yandex.mjs";
|
|
17
18
|
import { BaseOAuthProviderOptions } from "../../plugins/generic-oauth/index.mjs";
|
|
18
19
|
import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "../../plugins/jwt/types.mjs";
|
|
19
20
|
import { AuthorizationQuery, Client, CodeVerificationValue, OAuthAccessToken, OIDCMetadata, OIDCOptions, TokenBody } from "../../plugins/oidc-provider/types.mjs";
|
|
@@ -56,4 +57,4 @@ import { phoneNumberClient } from "../../plugins/phone-number/client.mjs";
|
|
|
56
57
|
import { siweClient } from "../../plugins/siwe/client.mjs";
|
|
57
58
|
import { usernameClient } from "../../plugins/username/client.mjs";
|
|
58
59
|
import { InferServerPlugin } from "./infer-plugin.mjs";
|
|
59
|
-
export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminClientOptions, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, type FieldAttributeToObject, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_ERROR_CODES, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationClientOptions, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, POPUP_TOKEN_STORAGE_KEY, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, type RemoveFieldsWithReturnedFalse, RequiredKeysOf, SessionWithImpersonatedBy, SignInPopupOptions, SignInPopupResult, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, createSignInPopup, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, getStoredPopupToken, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oauthPopupClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, popupBearerFetchPlugin, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode };
|
|
60
|
+
export { ADMIN_ERROR_CODES, ANONYMOUS_ERROR_CODES, AdminClientOptions, AdminOptions, AnonymousOptions, AnonymousSession, Auth0Options, AuthorizationQuery, BackupCodeOptions, BaseOAuthProviderOptions, Client, CodeVerificationValue, EMAIL_OTP_ERROR_CODES, ExtractPluginField, type FieldAttributeToObject, GENERIC_OAUTH_ERROR_CODES, GenericOAuthConfig, GenericOAuthOptions, GoogleOneTapActionOptions, GoogleOneTapOptions, GsiButtonConfiguration, GumroadOptions, HasRequiredKeys, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginFieldFromTuple, InferServerPlugin, InferTeam, Invitation, InvitationInput, InvitationStatus, IsAny, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodClientConfig, LineOptions, MULTI_SESSION_ERROR_CODES, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_ERROR_CODES, OAuthAccessToken, OIDCMetadata, OIDCOptions, ORGANIZATION_ERROR_CODES, OTPOptions, OidcClientPlugin, OktaOptions, OneTimeTokenOptions, Organization, OrganizationClientOptions, OrganizationInput, OrganizationRole, OrganizationSchema, OverrideMerge, PHONE_NUMBER_ERROR_CODES, POPUP_TOKEN_STORAGE_KEY, PatreonOptions, PhoneNumberOptions, Prettify, PrettifyDeep, type RemoveFieldsWithReturnedFalse, RequiredKeysOf, SessionWithImpersonatedBy, SignInPopupOptions, SignInPopupResult, SlackOptions, StripEmptyObjects, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamInput, TeamMember, TeamMemberInput, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UnionToIntersection, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, YandexOptions, adminClient, anonymousClient, auth0, backupCode2fa, clientSideHasPermission, createSignInPopup, customSessionClient, defaultRolesSchema, deviceAuthorizationClient, emailOTPClient, encodeBackupCodes, generateBackupCodes, genericOAuthClient, getBackupCodes, getStoredPopupToken, gumroad, hubspot, inferAdditionalFields, inferOrgAdditionalFields, invitationSchema, invitationStatus, jwtClient, keycloak, lastLoginMethodClient, line, magicLinkClient, memberSchema, microsoftEntraId, multiSessionClient, oauthPopupClient, oidcClient, okta, oneTapClient, oneTimeTokenClient, organizationClient, organizationRoleSchema, organizationSchema, otp2fa, patreon, phoneNumberClient, popupBearerFetchPlugin, roleSchema, schema, siweClient, slack, teamMemberSchema, teamSchema, totp2fa, twoFactorClient, usernameClient, verifyBackupCode, yandex };
|
package/dist/db/index.d.mts
CHANGED
|
@@ -3,7 +3,8 @@ import { convertFromDB, convertToDB } from "./field-converter.mjs";
|
|
|
3
3
|
import { getSchema } from "./get-schema.mjs";
|
|
4
4
|
import { DatabaseHooksEntry, getWithHooks } from "./with-hooks.mjs";
|
|
5
5
|
import { createInternalAdapter } from "./internal-adapter.mjs";
|
|
6
|
+
import { revokeUnprovenAccountAccess } from "./revoke-unproven-account-access.mjs";
|
|
6
7
|
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
|
|
7
8
|
import { FieldAttributeToSchema, toZodSchema } from "./to-zod.mjs";
|
|
8
9
|
export * from "@better-auth/core/db";
|
|
9
|
-
export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
|
|
10
|
+
export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, revokeUnprovenAccountAccess, toZodSchema };
|
package/dist/db/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAc
|
|
|
4
4
|
import { convertFromDB, convertToDB } from "./field-converter.mjs";
|
|
5
5
|
import { getWithHooks } from "./with-hooks.mjs";
|
|
6
6
|
import { createInternalAdapter } from "./internal-adapter.mjs";
|
|
7
|
+
import { revokeUnprovenAccountAccess } from "./revoke-unproven-account-access.mjs";
|
|
7
8
|
import { toZodSchema } from "./to-zod.mjs";
|
|
8
9
|
export * from "@better-auth/core/db";
|
|
9
10
|
//#region src/db/index.ts
|
|
@@ -25,9 +26,10 @@ var db_exports = /* @__PURE__ */ __exportAll({
|
|
|
25
26
|
parseSessionOutput: () => parseSessionOutput,
|
|
26
27
|
parseUserInput: () => parseUserInput,
|
|
27
28
|
parseUserOutput: () => parseUserOutput,
|
|
29
|
+
revokeUnprovenAccountAccess: () => revokeUnprovenAccountAccess,
|
|
28
30
|
toZodSchema: () => toZodSchema
|
|
29
31
|
});
|
|
30
32
|
import * as import__better_auth_core_db from "@better-auth/core/db";
|
|
31
33
|
__reExport(db_exports, import__better_auth_core_db);
|
|
32
34
|
//#endregion
|
|
33
|
-
export { buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
|
|
35
|
+
export { buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseAdditionalUserInputFromProviderProfile, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, revokeUnprovenAccountAccess, toZodSchema };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GenericEndpointContext } from "@better-auth/core";
|
|
2
|
+
|
|
3
|
+
//#region src/db/revoke-unproven-account-access.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Strip every credential and session a pre-existing account accrued before
|
|
6
|
+
* control of its email was proven.
|
|
7
|
+
*
|
|
8
|
+
* An `emailVerified: false` row carries no proof that the password on it belongs
|
|
9
|
+
* to the mailbox owner. When an email-primary proof (magic link, email OTP)
|
|
10
|
+
* resolves to such a row, deleting the `credential` account and revoking standing
|
|
11
|
+
* sessions makes the verified owner inherit no password or session that predates
|
|
12
|
+
* the proof. Call this before flipping `emailVerified` and minting the owner's
|
|
13
|
+
* session; it no-ops if a concurrent flow has already verified the account.
|
|
14
|
+
*
|
|
15
|
+
* @param userId - The pre-existing, not-yet-verified user being promoted.
|
|
16
|
+
*/
|
|
17
|
+
declare function revokeUnprovenAccountAccess(ctx: GenericEndpointContext, userId: string): Promise<void>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { revokeUnprovenAccountAccess };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/db/revoke-unproven-account-access.ts
|
|
2
|
+
/**
|
|
3
|
+
* Strip every credential and session a pre-existing account accrued before
|
|
4
|
+
* control of its email was proven.
|
|
5
|
+
*
|
|
6
|
+
* An `emailVerified: false` row carries no proof that the password on it belongs
|
|
7
|
+
* to the mailbox owner. When an email-primary proof (magic link, email OTP)
|
|
8
|
+
* resolves to such a row, deleting the `credential` account and revoking standing
|
|
9
|
+
* sessions makes the verified owner inherit no password or session that predates
|
|
10
|
+
* the proof. Call this before flipping `emailVerified` and minting the owner's
|
|
11
|
+
* session; it no-ops if a concurrent flow has already verified the account.
|
|
12
|
+
*
|
|
13
|
+
* @param userId - The pre-existing, not-yet-verified user being promoted.
|
|
14
|
+
*/
|
|
15
|
+
async function revokeUnprovenAccountAccess(ctx, userId) {
|
|
16
|
+
const user = await ctx.context.internalAdapter.findUserById(userId);
|
|
17
|
+
if (!user || user.emailVerified) return;
|
|
18
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(userId);
|
|
19
|
+
for (const account of accounts) if (account.providerId === "credential") await ctx.context.internalAdapter.deleteAccount(account.id);
|
|
20
|
+
await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { revokeUnprovenAccountAccess };
|
package/dist/package.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { getDate } from "../../utils/date.mjs";
|
|
|
3
3
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
4
|
import { symmetricDecrypt } from "../../crypto/index.mjs";
|
|
5
5
|
import { setCookieCache, setSessionCookie } from "../../cookies/index.mjs";
|
|
6
|
+
import { revokeUnprovenAccountAccess } from "../../db/revoke-unproven-account-access.mjs";
|
|
6
7
|
import { getSessionFromCtx, sensitiveSessionMiddleware } from "../../api/routes/session.mjs";
|
|
7
8
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
8
9
|
import { EMAIL_OTP_ERROR_CODES } from "./error-codes.mjs";
|
|
@@ -420,7 +421,10 @@ const signInEmailOTP = (opts) => createAuthEndpoint("/sign-in/email-otp", {
|
|
|
420
421
|
user: parseUserOutput(ctx.context.options, newUser)
|
|
421
422
|
});
|
|
422
423
|
}
|
|
423
|
-
if (!user.user.emailVerified)
|
|
424
|
+
if (!user.user.emailVerified) {
|
|
425
|
+
await revokeUnprovenAccountAccess(ctx, user.user.id);
|
|
426
|
+
await ctx.context.internalAdapter.updateUser(user.user.id, { emailVerified: true });
|
|
427
|
+
}
|
|
424
428
|
const session = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
425
429
|
await setSessionCookie(ctx, {
|
|
426
430
|
session,
|
|
@@ -8,6 +8,7 @@ import { MicrosoftEntraIdOptions, microsoftEntraId } from "./providers/microsoft
|
|
|
8
8
|
import { OktaOptions, okta } from "./providers/okta.mjs";
|
|
9
9
|
import { PatreonOptions, patreon } from "./providers/patreon.mjs";
|
|
10
10
|
import { SlackOptions, slack } from "./providers/slack.mjs";
|
|
11
|
+
import { YandexOptions, yandex } from "./providers/yandex.mjs";
|
|
11
12
|
import { BaseOAuthProviderOptions, genericOAuth } from "./index.mjs";
|
|
12
13
|
import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes.mjs";
|
|
13
14
|
import * as _better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
|
|
@@ -8,6 +8,7 @@ import { MicrosoftEntraIdOptions, microsoftEntraId } from "./providers/microsoft
|
|
|
8
8
|
import { OktaOptions, okta } from "./providers/okta.mjs";
|
|
9
9
|
import { PatreonOptions, patreon } from "./providers/patreon.mjs";
|
|
10
10
|
import { SlackOptions, slack } from "./providers/slack.mjs";
|
|
11
|
+
import { YandexOptions, yandex } from "./providers/yandex.mjs";
|
|
11
12
|
import { AuthContext } from "@better-auth/core";
|
|
12
13
|
import * as _better_auth_core_oauth20 from "@better-auth/core/oauth2";
|
|
13
14
|
import { OAuthProvider } from "@better-auth/core/oauth2";
|
|
@@ -197,4 +198,4 @@ declare const genericOAuth: (options: GenericOAuthOptions) => {
|
|
|
197
198
|
};
|
|
198
199
|
};
|
|
199
200
|
//#endregion
|
|
200
|
-
export { Auth0Options, BaseOAuthProviderOptions, type GenericOAuthConfig, type GenericOAuthOptions, GumroadOptions, HubSpotOptions, KeycloakOptions, LineOptions, MicrosoftEntraIdOptions, OktaOptions, PatreonOptions, SlackOptions, auth0, genericOAuth, gumroad, hubspot, keycloak, line, microsoftEntraId, okta, patreon, slack };
|
|
201
|
+
export { Auth0Options, BaseOAuthProviderOptions, type GenericOAuthConfig, type GenericOAuthOptions, GumroadOptions, HubSpotOptions, KeycloakOptions, LineOptions, MicrosoftEntraIdOptions, OktaOptions, PatreonOptions, SlackOptions, YandexOptions, auth0, genericOAuth, gumroad, hubspot, keycloak, line, microsoftEntraId, okta, patreon, slack, yandex };
|
|
@@ -10,6 +10,7 @@ import { microsoftEntraId } from "./providers/microsoft-entra-id.mjs";
|
|
|
10
10
|
import { okta } from "./providers/okta.mjs";
|
|
11
11
|
import { patreon } from "./providers/patreon.mjs";
|
|
12
12
|
import { slack } from "./providers/slack.mjs";
|
|
13
|
+
import { yandex } from "./providers/yandex.mjs";
|
|
13
14
|
import { APIError } from "@better-auth/core/error";
|
|
14
15
|
import { applyDefaultAccessTokenExpiry, createAuthorizationURL, refreshAccessToken, validateAuthorizationCode } from "@better-auth/core/oauth2";
|
|
15
16
|
import { betterFetch } from "@better-fetch/fetch";
|
|
@@ -145,4 +146,4 @@ const genericOAuth = (options) => {
|
|
|
145
146
|
};
|
|
146
147
|
};
|
|
147
148
|
//#endregion
|
|
148
|
-
export { auth0, genericOAuth, gumroad, hubspot, keycloak, line, microsoftEntraId, okta, patreon, slack };
|
|
149
|
+
export { auth0, genericOAuth, gumroad, hubspot, keycloak, line, microsoftEntraId, okta, patreon, slack, yandex };
|
|
@@ -6,4 +6,5 @@ import { LineOptions, line } from "./line.mjs";
|
|
|
6
6
|
import { MicrosoftEntraIdOptions, microsoftEntraId } from "./microsoft-entra-id.mjs";
|
|
7
7
|
import { OktaOptions, okta } from "./okta.mjs";
|
|
8
8
|
import { PatreonOptions, patreon } from "./patreon.mjs";
|
|
9
|
-
import { SlackOptions, slack } from "./slack.mjs";
|
|
9
|
+
import { SlackOptions, slack } from "./slack.mjs";
|
|
10
|
+
import { YandexOptions, yandex } from "./yandex.mjs";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { GenericOAuthConfig } from "../types.mjs";
|
|
2
|
+
import { BaseOAuthProviderOptions } from "../index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/generic-oauth/providers/yandex.d.ts
|
|
5
|
+
interface YandexOptions extends BaseOAuthProviderOptions {}
|
|
6
|
+
/**
|
|
7
|
+
* Yandex OAuth provider helper
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { genericOAuth, yandex } from "better-auth/plugins/generic-oauth";
|
|
12
|
+
*
|
|
13
|
+
* export const auth = betterAuth({
|
|
14
|
+
* plugins: [
|
|
15
|
+
* genericOAuth({
|
|
16
|
+
* config: [
|
|
17
|
+
* yandex({
|
|
18
|
+
* clientId: process.env.YANDEX_CLIENT_ID,
|
|
19
|
+
* clientSecret: process.env.YANDEX_CLIENT_SECRET,
|
|
20
|
+
* }),
|
|
21
|
+
* ],
|
|
22
|
+
* }),
|
|
23
|
+
* ],
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function yandex(options: YandexOptions): GenericOAuthConfig;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { YandexOptions, yandex };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
+
//#region src/plugins/generic-oauth/providers/yandex.ts
|
|
3
|
+
/**
|
|
4
|
+
* Yandex OAuth provider helper
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { genericOAuth, yandex } from "better-auth/plugins/generic-oauth";
|
|
9
|
+
*
|
|
10
|
+
* export const auth = betterAuth({
|
|
11
|
+
* plugins: [
|
|
12
|
+
* genericOAuth({
|
|
13
|
+
* config: [
|
|
14
|
+
* yandex({
|
|
15
|
+
* clientId: process.env.YANDEX_CLIENT_ID,
|
|
16
|
+
* clientSecret: process.env.YANDEX_CLIENT_SECRET,
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* }),
|
|
20
|
+
* ],
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function yandex(options) {
|
|
25
|
+
const defaultScopes = [
|
|
26
|
+
"login:info",
|
|
27
|
+
"login:email",
|
|
28
|
+
"login:avatar"
|
|
29
|
+
];
|
|
30
|
+
const getUserInfo = async (tokens) => {
|
|
31
|
+
const { data: profile, error } = await betterFetch("https://login.yandex.ru/info?format=json", {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: { Authorization: `OAuth ${tokens.accessToken}` }
|
|
34
|
+
});
|
|
35
|
+
if (error || !profile) return null;
|
|
36
|
+
return {
|
|
37
|
+
id: profile.id,
|
|
38
|
+
name: profile.display_name ?? profile.real_name ?? profile.first_name ?? profile.login,
|
|
39
|
+
email: profile.default_email ?? profile.emails?.[0],
|
|
40
|
+
emailVerified: false,
|
|
41
|
+
image: !profile.is_avatar_empty && profile.default_avatar_id ? `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200` : void 0
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
providerId: "yandex",
|
|
46
|
+
authorizationUrl: "https://oauth.yandex.com/authorize",
|
|
47
|
+
tokenUrl: "https://oauth.yandex.com/token",
|
|
48
|
+
clientId: options.clientId,
|
|
49
|
+
clientSecret: options.clientSecret,
|
|
50
|
+
scopes: options.scopes ?? defaultScopes,
|
|
51
|
+
redirectURI: options.redirectURI,
|
|
52
|
+
pkce: options.pkce,
|
|
53
|
+
disableImplicitSignUp: options.disableImplicitSignUp,
|
|
54
|
+
disableSignUp: options.disableSignUp,
|
|
55
|
+
overrideUserInfo: options.overrideUserInfo,
|
|
56
|
+
getUserInfo
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { yandex };
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -27,6 +27,7 @@ import { MicrosoftEntraIdOptions, microsoftEntraId } from "./generic-oauth/provi
|
|
|
27
27
|
import { OktaOptions, okta } from "./generic-oauth/providers/okta.mjs";
|
|
28
28
|
import { PatreonOptions, patreon } from "./generic-oauth/providers/patreon.mjs";
|
|
29
29
|
import { SlackOptions, slack } from "./generic-oauth/providers/slack.mjs";
|
|
30
|
+
import { YandexOptions, yandex } from "./generic-oauth/providers/yandex.mjs";
|
|
30
31
|
import { BaseOAuthProviderOptions, genericOAuth } from "./generic-oauth/index.mjs";
|
|
31
32
|
import { HaveIBeenPwnedOptions, haveIBeenPwned } from "./haveibeenpwned/index.mjs";
|
|
32
33
|
import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions } from "./jwt/types.mjs";
|
|
@@ -66,4 +67,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
|
|
|
66
67
|
import { UsernameOptions, username } from "./username/index.mjs";
|
|
67
68
|
import { hasPermission } from "./organization/has-permission.mjs";
|
|
68
69
|
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
|
|
69
|
-
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, 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, OAuthAccessToken, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth };
|
|
70
|
+
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizationQuery, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, Client, CloudflareTurnstileOptions, CodeVerificationValue, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationOptions, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, 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, OAuthAccessToken, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OIDCMetadata, OIDCOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TokenBody, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, memberSchema, microsoftEntraId, ms, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, withMcpAuth, yandex };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { microsoftEntraId } from "./generic-oauth/providers/microsoft-entra-id.m
|
|
|
22
22
|
import { okta } from "./generic-oauth/providers/okta.mjs";
|
|
23
23
|
import { patreon } from "./generic-oauth/providers/patreon.mjs";
|
|
24
24
|
import { slack } from "./generic-oauth/providers/slack.mjs";
|
|
25
|
+
import { yandex } from "./generic-oauth/providers/yandex.mjs";
|
|
25
26
|
import { genericOAuth } from "./generic-oauth/index.mjs";
|
|
26
27
|
import { haveIBeenPwned } from "./haveibeenpwned/index.mjs";
|
|
27
28
|
import { createJwk, generateExportedKeyPair, toExpJWT } from "./jwt/utils.mjs";
|
|
@@ -46,4 +47,4 @@ import { siwe } from "./siwe/index.mjs";
|
|
|
46
47
|
import { testUtils } from "./test-utils/index.mjs";
|
|
47
48
|
import { twoFactor } from "./two-factor/index.mjs";
|
|
48
49
|
import { username } from "./username/index.mjs";
|
|
49
|
-
export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, POPUP_MARKER_COOKIE, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth };
|
|
50
|
+
export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, POPUP_MARKER_COOKIE, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getClient, getJwtToken, getMCPProtectedResourceMetadata, getMCPProviderMetadata, getMetadata, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, mcp, microsoftEntraId, multiSession, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, oAuthProxy, oauthPopup, oidcProvider, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, withMcpAuth, yandex };
|
|
@@ -2,6 +2,7 @@ import { originCheck } from "../../api/middlewares/origin-check.mjs";
|
|
|
2
2
|
import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
|
|
3
3
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
|
+
import { revokeUnprovenAccountAccess } from "../../db/revoke-unproven-account-access.mjs";
|
|
5
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
6
7
|
import { defaultKeyHasher } from "./utils.mjs";
|
|
7
8
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -134,7 +135,10 @@ const magicLink = (options) => {
|
|
|
134
135
|
user = newUser;
|
|
135
136
|
if (!user) redirectWithError("failed_to_create_user");
|
|
136
137
|
} else redirectWithError("new_user_signup_disabled");
|
|
137
|
-
if (!user.emailVerified)
|
|
138
|
+
if (!user.emailVerified) {
|
|
139
|
+
await revokeUnprovenAccountAccess(ctx, user.id);
|
|
140
|
+
user = await ctx.context.internalAdapter.updateUser(user.id, { emailVerified: true });
|
|
141
|
+
}
|
|
138
142
|
const session = await ctx.context.internalAdapter.createSession(user.id);
|
|
139
143
|
if (!session) redirectWithError("failed_to_create_session");
|
|
140
144
|
await setSessionCookie(ctx, {
|
|
@@ -5,7 +5,7 @@ import { sessionMiddleware } from "../../../api/routes/session.mjs";
|
|
|
5
5
|
import { shouldRequirePassword } from "../../../utils/password.mjs";
|
|
6
6
|
import { PACKAGE_VERSION } from "../../../version.mjs";
|
|
7
7
|
import { TWO_FACTOR_ERROR_CODES } from "../error-code.mjs";
|
|
8
|
-
import { verifyTwoFactor } from "../verify-two-factor.mjs";
|
|
8
|
+
import { assertTwoFactorNotLocked, recordTwoFactorFailure, resetTwoFactorFailures, verifyTwoFactor } from "../verify-two-factor.mjs";
|
|
9
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
10
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
11
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -172,6 +172,7 @@ const backupCode2fa = (opts) => {
|
|
|
172
172
|
}]
|
|
173
173
|
});
|
|
174
174
|
if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.BACKUP_CODES_NOT_ENABLED);
|
|
175
|
+
if (isSignIn) await assertTwoFactorNotLocked(ctx, twoFactorTable, twoFactor);
|
|
175
176
|
const attempt = isSignIn ? await beginAttempt(5) : null;
|
|
176
177
|
let validate;
|
|
177
178
|
try {
|
|
@@ -185,6 +186,7 @@ const backupCode2fa = (opts) => {
|
|
|
185
186
|
}
|
|
186
187
|
if (!validate.status || !validate.updated) {
|
|
187
188
|
await attempt?.recordFailure();
|
|
189
|
+
if (isSignIn) await recordTwoFactorFailure(ctx, twoFactorTable, twoFactor);
|
|
188
190
|
throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES.INVALID_BACKUP_CODE);
|
|
189
191
|
}
|
|
190
192
|
const updatedBackupCodes = await encodeBackupCodes(validate.updated, ctx.context.secretConfig, opts);
|
|
@@ -200,6 +202,7 @@ const backupCode2fa = (opts) => {
|
|
|
200
202
|
increment: {},
|
|
201
203
|
set: { backupCodes: updatedBackupCodes }
|
|
202
204
|
})) throw APIError.fromStatus("CONFLICT", { message: "Failed to verify backup code. Please try again." });
|
|
205
|
+
if (isSignIn) await resetTwoFactorFailures(ctx, twoFactorTable, twoFactor);
|
|
203
206
|
if (!ctx.body.disableSession) return valid(ctx);
|
|
204
207
|
return ctx.json({
|
|
205
208
|
token: session.session?.token,
|
|
@@ -65,6 +65,7 @@ declare const twoFactorClient: (options?: {
|
|
|
65
65
|
INVALID_BACKUP_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_BACKUP_CODE">;
|
|
66
66
|
INVALID_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_CODE">;
|
|
67
67
|
TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: _better_auth_core_utils_error_codes0.RawError<"TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE">;
|
|
68
|
+
ACCOUNT_TEMPORARILY_LOCKED: _better_auth_core_utils_error_codes0.RawError<"ACCOUNT_TEMPORARILY_LOCKED">;
|
|
68
69
|
INVALID_TWO_FACTOR_COOKIE: _better_auth_core_utils_error_codes0.RawError<"INVALID_TWO_FACTOR_COOKIE">;
|
|
69
70
|
};
|
|
70
71
|
};
|
|
@@ -10,6 +10,7 @@ declare const TWO_FACTOR_ERROR_CODES: {
|
|
|
10
10
|
INVALID_BACKUP_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_BACKUP_CODE">;
|
|
11
11
|
INVALID_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_CODE">;
|
|
12
12
|
TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: _better_auth_core_utils_error_codes0.RawError<"TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE">;
|
|
13
|
+
ACCOUNT_TEMPORARILY_LOCKED: _better_auth_core_utils_error_codes0.RawError<"ACCOUNT_TEMPORARILY_LOCKED">;
|
|
13
14
|
INVALID_TWO_FACTOR_COOKIE: _better_auth_core_utils_error_codes0.RawError<"INVALID_TWO_FACTOR_COOKIE">;
|
|
14
15
|
};
|
|
15
16
|
//#endregion
|
|
@@ -9,6 +9,7 @@ const TWO_FACTOR_ERROR_CODES = defineErrorCodes({
|
|
|
9
9
|
INVALID_BACKUP_CODE: "Invalid backup code",
|
|
10
10
|
INVALID_CODE: "Invalid code",
|
|
11
11
|
TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: "Too many attempts. Please request a new code.",
|
|
12
|
+
ACCOUNT_TEMPORARILY_LOCKED: "Too many failed verification attempts. Your account is temporarily locked. Please try again later.",
|
|
12
13
|
INVALID_TWO_FACTOR_COOKIE: "Invalid two factor cookie"
|
|
13
14
|
});
|
|
14
15
|
//#endregion
|
|
@@ -662,6 +662,19 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
662
662
|
defaultValue: true;
|
|
663
663
|
input: false;
|
|
664
664
|
};
|
|
665
|
+
failedVerificationCount: {
|
|
666
|
+
type: "number";
|
|
667
|
+
required: false;
|
|
668
|
+
defaultValue: number;
|
|
669
|
+
input: false;
|
|
670
|
+
returned: false;
|
|
671
|
+
};
|
|
672
|
+
lockedUntil: {
|
|
673
|
+
type: "date";
|
|
674
|
+
required: false;
|
|
675
|
+
input: false;
|
|
676
|
+
returned: false;
|
|
677
|
+
};
|
|
665
678
|
};
|
|
666
679
|
};
|
|
667
680
|
};
|
|
@@ -679,6 +692,7 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
679
692
|
INVALID_BACKUP_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_BACKUP_CODE">;
|
|
680
693
|
INVALID_CODE: _better_auth_core_utils_error_codes0.RawError<"INVALID_CODE">;
|
|
681
694
|
TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: _better_auth_core_utils_error_codes0.RawError<"TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE">;
|
|
695
|
+
ACCOUNT_TEMPORARILY_LOCKED: _better_auth_core_utils_error_codes0.RawError<"ACCOUNT_TEMPORARILY_LOCKED">;
|
|
682
696
|
INVALID_TWO_FACTOR_COOKIE: _better_auth_core_utils_error_codes0.RawError<"INVALID_TWO_FACTOR_COOKIE">;
|
|
683
697
|
};
|
|
684
698
|
};
|
|
@@ -5,7 +5,7 @@ import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto/index.mjs";
|
|
|
5
5
|
import { setSessionCookie } from "../../../cookies/index.mjs";
|
|
6
6
|
import { PACKAGE_VERSION } from "../../../version.mjs";
|
|
7
7
|
import { TWO_FACTOR_ERROR_CODES } from "../error-code.mjs";
|
|
8
|
-
import { verifyTwoFactor } from "../verify-two-factor.mjs";
|
|
8
|
+
import { assertTwoFactorNotLocked, recordTwoFactorFailure, resetTwoFactorFailures, verifyTwoFactor } from "../verify-two-factor.mjs";
|
|
9
9
|
import { defaultKeyHasher } from "../utils.mjs";
|
|
10
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -158,6 +158,20 @@ const otp2fa = (options) => {
|
|
|
158
158
|
} }
|
|
159
159
|
}, async (ctx) => {
|
|
160
160
|
const { session, key, valid, invalid } = await verifyTwoFactor(ctx);
|
|
161
|
+
const isSignIn = !session.session;
|
|
162
|
+
const twoFactorTable = "twoFactor";
|
|
163
|
+
let twoFactor = null;
|
|
164
|
+
if (isSignIn) {
|
|
165
|
+
twoFactor = await ctx.context.adapter.findOne({
|
|
166
|
+
model: twoFactorTable,
|
|
167
|
+
where: [{
|
|
168
|
+
field: "userId",
|
|
169
|
+
value: session.user.id
|
|
170
|
+
}]
|
|
171
|
+
});
|
|
172
|
+
if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TWO_FACTOR_NOT_ENABLED);
|
|
173
|
+
await assertTwoFactorNotLocked(ctx, twoFactorTable, twoFactor);
|
|
174
|
+
}
|
|
161
175
|
const consumed = await ctx.context.internalAdapter.consumeVerificationValue(`2fa-otp-${key}`);
|
|
162
176
|
if (!consumed) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED);
|
|
163
177
|
const [otp, counter] = consumed.value?.split(":") ?? [];
|
|
@@ -166,6 +180,7 @@ const otp2fa = (options) => {
|
|
|
166
180
|
if (attempts >= allowedAttempts) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE);
|
|
167
181
|
const [storedValue, inputValue] = await decryptOrHashForComparison(ctx, otp, ctx.body.code);
|
|
168
182
|
if (constantTimeEqual(new TextEncoder().encode(storedValue), new TextEncoder().encode(inputValue))) {
|
|
183
|
+
if (twoFactor) await resetTwoFactorFailures(ctx, twoFactorTable, twoFactor);
|
|
169
184
|
if (!session.user.twoFactorEnabled) {
|
|
170
185
|
if (!session.session) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
171
186
|
const updatedUser = await ctx.context.internalAdapter.updateUser(session.user.id, { twoFactorEnabled: true });
|
|
@@ -187,6 +202,7 @@ const otp2fa = (options) => {
|
|
|
187
202
|
identifier: `2fa-otp-${key}`,
|
|
188
203
|
expiresAt: consumed.expiresAt
|
|
189
204
|
});
|
|
205
|
+
if (twoFactor) await recordTwoFactorFailure(ctx, twoFactorTable, twoFactor);
|
|
190
206
|
return invalid("INVALID_CODE");
|
|
191
207
|
})
|
|
192
208
|
}
|
|
@@ -39,6 +39,19 @@ declare const schema: {
|
|
|
39
39
|
defaultValue: true;
|
|
40
40
|
input: false;
|
|
41
41
|
};
|
|
42
|
+
failedVerificationCount: {
|
|
43
|
+
type: "number";
|
|
44
|
+
required: false;
|
|
45
|
+
defaultValue: number;
|
|
46
|
+
input: false;
|
|
47
|
+
returned: false;
|
|
48
|
+
};
|
|
49
|
+
lockedUntil: {
|
|
50
|
+
type: "date";
|
|
51
|
+
required: false;
|
|
52
|
+
input: false;
|
|
53
|
+
returned: false;
|
|
54
|
+
};
|
|
42
55
|
};
|
|
43
56
|
};
|
|
44
57
|
};
|
|
@@ -33,6 +33,19 @@ const schema = {
|
|
|
33
33
|
required: false,
|
|
34
34
|
defaultValue: true,
|
|
35
35
|
input: false
|
|
36
|
+
},
|
|
37
|
+
failedVerificationCount: {
|
|
38
|
+
type: "number",
|
|
39
|
+
required: false,
|
|
40
|
+
defaultValue: 0,
|
|
41
|
+
input: false,
|
|
42
|
+
returned: false
|
|
43
|
+
},
|
|
44
|
+
lockedUntil: {
|
|
45
|
+
type: "date",
|
|
46
|
+
required: false,
|
|
47
|
+
input: false,
|
|
48
|
+
returned: false
|
|
36
49
|
}
|
|
37
50
|
} }
|
|
38
51
|
};
|
|
@@ -4,7 +4,7 @@ import { sessionMiddleware } from "../../../api/routes/session.mjs";
|
|
|
4
4
|
import { shouldRequirePassword } from "../../../utils/password.mjs";
|
|
5
5
|
import { PACKAGE_VERSION } from "../../../version.mjs";
|
|
6
6
|
import { TWO_FACTOR_ERROR_CODES } from "../error-code.mjs";
|
|
7
|
-
import { verifyTwoFactor } from "../verify-two-factor.mjs";
|
|
7
|
+
import { assertTwoFactorNotLocked, recordTwoFactorFailure, resetTwoFactorFailures, verifyTwoFactor } from "../verify-two-factor.mjs";
|
|
8
8
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
9
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
10
10
|
import * as z from "zod";
|
|
@@ -134,6 +134,7 @@ const totp2fa = (options) => {
|
|
|
134
134
|
});
|
|
135
135
|
if (!twoFactor) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED);
|
|
136
136
|
if (isSignIn && twoFactor.verified === false) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED);
|
|
137
|
+
if (isSignIn) await assertTwoFactorNotLocked(ctx, twoFactorTable, twoFactor);
|
|
137
138
|
const attempt = isSignIn ? await beginAttempt(5) : null;
|
|
138
139
|
let status;
|
|
139
140
|
try {
|
|
@@ -150,8 +151,10 @@ const totp2fa = (options) => {
|
|
|
150
151
|
}
|
|
151
152
|
if (!status) {
|
|
152
153
|
await attempt?.recordFailure();
|
|
154
|
+
if (isSignIn) await recordTwoFactorFailure(ctx, twoFactorTable, twoFactor);
|
|
153
155
|
return invalid("INVALID_CODE");
|
|
154
156
|
}
|
|
157
|
+
if (isSignIn) await resetTwoFactorFailures(ctx, twoFactorTable, twoFactor);
|
|
155
158
|
if (twoFactor.verified !== true) {
|
|
156
159
|
if (!user.twoFactorEnabled) {
|
|
157
160
|
const activeSession = session.session;
|
|
@@ -63,6 +63,29 @@ interface TwoFactorOptions {
|
|
|
63
63
|
* @default 2592000 (30 days)
|
|
64
64
|
*/
|
|
65
65
|
trustDeviceMaxAge?: number | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Account-level lockout for failed second-factor verifications during
|
|
68
|
+
* sign-in. Caps consecutive failed attempts per account, across challenges
|
|
69
|
+
* and factors, and resets on a successful verification.
|
|
70
|
+
*/
|
|
71
|
+
accountLockout?: {
|
|
72
|
+
/**
|
|
73
|
+
* Whether account-level lockout is enforced.
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
enabled?: boolean | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Consecutive failed verifications, across challenges and factors,
|
|
79
|
+
* before the account is locked.
|
|
80
|
+
* @default 10
|
|
81
|
+
*/
|
|
82
|
+
maxFailedAttempts?: number | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* How long the account stays locked, in seconds.
|
|
85
|
+
* @default 900 (15 minutes)
|
|
86
|
+
*/
|
|
87
|
+
durationSeconds?: number | undefined;
|
|
88
|
+
} | undefined;
|
|
66
89
|
}
|
|
67
90
|
interface UserWithTwoFactor extends User {
|
|
68
91
|
/**
|
|
@@ -81,6 +104,8 @@ interface TwoFactorTable {
|
|
|
81
104
|
secret: string;
|
|
82
105
|
backupCodes: string;
|
|
83
106
|
verified: boolean;
|
|
107
|
+
failedVerificationCount?: number;
|
|
108
|
+
lockedUntil?: Date | null;
|
|
84
109
|
}
|
|
85
110
|
//#endregion
|
|
86
111
|
export { TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor };
|
|
@@ -106,5 +106,87 @@ async function verifyTwoFactor(ctx) {
|
|
|
106
106
|
})
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
|
+
function resolveAccountLockoutConfig(ctx) {
|
|
110
|
+
const lockout = (ctx.context.getPlugin("two-factor")?.options)?.accountLockout;
|
|
111
|
+
return {
|
|
112
|
+
enabled: lockout?.enabled ?? true,
|
|
113
|
+
maxFailedAttempts: lockout?.maxFailedAttempts ?? 10,
|
|
114
|
+
durationMs: (lockout?.durationSeconds ?? 900) * 1e3
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Reject the verification when the account is locked, and lazily clear an
|
|
119
|
+
* expired lock. The lock caps consecutive failed verifications per account,
|
|
120
|
+
* across challenges and factors (NIST SP 800-63B §5.2.2).
|
|
121
|
+
*/
|
|
122
|
+
async function assertTwoFactorNotLocked(ctx, twoFactorTable, twoFactor) {
|
|
123
|
+
const { enabled } = resolveAccountLockoutConfig(ctx);
|
|
124
|
+
if (!enabled || !twoFactor.lockedUntil) return;
|
|
125
|
+
if (new Date(twoFactor.lockedUntil).getTime() > Date.now()) throw APIError.from("TOO_MANY_REQUESTS", TWO_FACTOR_ERROR_CODES.ACCOUNT_TEMPORARILY_LOCKED);
|
|
126
|
+
await ctx.context.adapter.incrementOne({
|
|
127
|
+
model: twoFactorTable,
|
|
128
|
+
where: [{
|
|
129
|
+
field: "id",
|
|
130
|
+
value: twoFactor.id
|
|
131
|
+
}, {
|
|
132
|
+
field: "lockedUntil",
|
|
133
|
+
operator: "lte",
|
|
134
|
+
value: /* @__PURE__ */ new Date()
|
|
135
|
+
}],
|
|
136
|
+
increment: {},
|
|
137
|
+
set: {
|
|
138
|
+
failedVerificationCount: 0,
|
|
139
|
+
lockedUntil: null
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Count one failed verification toward the account-level budget, and lock the
|
|
145
|
+
* account once the budget is spent. The increment is atomic, so concurrent
|
|
146
|
+
* failures cannot lose updates. It is unguarded so it still applies to a row
|
|
147
|
+
* whose counter is null or absent (a row created before the migration, or a
|
|
148
|
+
* document-store record predating the column), where a guarded comparison
|
|
149
|
+
* would never match.
|
|
150
|
+
*/
|
|
151
|
+
async function recordTwoFactorFailure(ctx, twoFactorTable, twoFactor) {
|
|
152
|
+
const { enabled, maxFailedAttempts, durationMs } = resolveAccountLockoutConfig(ctx);
|
|
153
|
+
if (!enabled) return;
|
|
154
|
+
if (((await ctx.context.adapter.incrementOne({
|
|
155
|
+
model: twoFactorTable,
|
|
156
|
+
where: [{
|
|
157
|
+
field: "id",
|
|
158
|
+
value: twoFactor.id
|
|
159
|
+
}],
|
|
160
|
+
increment: { failedVerificationCount: 1 }
|
|
161
|
+
}))?.failedVerificationCount ?? 0) >= maxFailedAttempts) await ctx.context.adapter.update({
|
|
162
|
+
model: twoFactorTable,
|
|
163
|
+
where: [{
|
|
164
|
+
field: "id",
|
|
165
|
+
value: twoFactor.id
|
|
166
|
+
}],
|
|
167
|
+
update: { lockedUntil: new Date(Date.now() + durationMs) }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Clear the account-level failure budget after a successful verification, so the
|
|
172
|
+
* count tracks only consecutive failures. The write is unconditional: a snapshot
|
|
173
|
+
* read at the start of the request can miss a concurrent failure, so skipping it
|
|
174
|
+
* could leave the counter non-zero after a success.
|
|
175
|
+
*/
|
|
176
|
+
async function resetTwoFactorFailures(ctx, twoFactorTable, twoFactor) {
|
|
177
|
+
const { enabled } = resolveAccountLockoutConfig(ctx);
|
|
178
|
+
if (!enabled) return;
|
|
179
|
+
await ctx.context.adapter.update({
|
|
180
|
+
model: twoFactorTable,
|
|
181
|
+
where: [{
|
|
182
|
+
field: "id",
|
|
183
|
+
value: twoFactor.id
|
|
184
|
+
}],
|
|
185
|
+
update: {
|
|
186
|
+
failedVerificationCount: 0,
|
|
187
|
+
lockedUntil: null
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
109
191
|
//#endregion
|
|
110
|
-
export { verifyTwoFactor };
|
|
192
|
+
export { assertTwoFactorNotLocked, recordTwoFactorFailure, resetTwoFactorFailures, verifyTwoFactor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.23",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -489,13 +489,13 @@
|
|
|
489
489
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
490
490
|
"nanostores": "^1.1.1",
|
|
491
491
|
"zod": "^4.3.6",
|
|
492
|
-
"@better-auth/core": "1.6.
|
|
493
|
-
"@better-auth/drizzle-adapter": "1.6.
|
|
494
|
-
"@better-auth/kysely-adapter": "1.6.
|
|
495
|
-
"@better-auth/memory-adapter": "1.6.
|
|
496
|
-
"@better-auth/mongo-adapter": "1.6.
|
|
497
|
-
"@better-auth/prisma-adapter": "1.6.
|
|
498
|
-
"@better-auth/telemetry": "1.6.
|
|
492
|
+
"@better-auth/core": "1.6.23",
|
|
493
|
+
"@better-auth/drizzle-adapter": "1.6.23",
|
|
494
|
+
"@better-auth/kysely-adapter": "1.6.23",
|
|
495
|
+
"@better-auth/memory-adapter": "1.6.23",
|
|
496
|
+
"@better-auth/mongo-adapter": "1.6.23",
|
|
497
|
+
"@better-auth/prisma-adapter": "1.6.23",
|
|
498
|
+
"@better-auth/telemetry": "1.6.23"
|
|
499
499
|
},
|
|
500
500
|
"devDependencies": {
|
|
501
501
|
"@lynx-js/react": "^0.116.3",
|