import { ConvexCredentials, type ConvexCredentialsUserConfig, } from "@convex-dev/auth/providers/ConvexCredentials"; import { type EmailConfig, type GenericActionCtxWithAuthConfig, type GenericDoc, createAccount, invalidateSessions, modifyAccountCredentials, retrieveAccount, signInViaProvider, } from "@convex-dev/auth/server"; import type { DocumentByName, GenericDataModel, WithoutSystemFields, } from "convex/server"; import type { Value } from "convex/values"; import { Scrypt } from "lucia"; export type PasswordConfig = { id?: string; /** * Perform checks on provided params and customize the user * information stored after sign up, including email normalization. * * Called for every flow ("signUp", "signIn", "reset", * "reset-verification" and "email-verification"). */ profile?: ( /** * The values passed to the `signIn` function. */ params: Record, /** * Convex ActionCtx in case you want to read from or write to * the database. */ ctx: GenericActionCtxWithAuthConfig, ) => WithoutSystemFields> & { email: string; }; /** * Performs custom validation on password provided during sign up or reset. * * Otherwise the default validation is used (password is not empty and * at least 8 characters in length). * * If the provided password is invalid, implementations must throw an Error. * * @param password the password supplied during "signUp" or * "reset-verification" flows. */ validatePasswordRequirements?: (password: string) => void; /** * Provide hashing and verification functions if you want to control * how passwords are hashed. */ crypto?: ConvexCredentialsUserConfig["crypto"]; /** * An Auth.js email provider used to require verification * before password reset. */ reset?: EmailConfig | ((...args: any) => EmailConfig); /** * An Auth.js email provider used to require verification * before sign up / sign in. */ verify?: EmailConfig | ((...args: any) => EmailConfig); } /** * Email and password authentication provider. * * Passwords are by default hashed using Scrypt from Lucia. * You can customize the hashing via the `crypto` option. * * Email verification is not required unless you pass * an email provider to the `verify` option. */ export function Password( config: PasswordConfig = {}, ) { const provider = config.id ?? "password"; return ConvexCredentials({ id: "password", authorize: async (params, ctx) => { const flow = params.flow as string; const passwordToValidate = flow === "signUp" ? (params.password as string) : flow === "reset-verification" ? (params.newPassword as string) : null; if (passwordToValidate !== null) { if (config.validatePasswordRequirements !== undefined) { config.validatePasswordRequirements(passwordToValidate); } else { validateDefaultPasswordRequirements(passwordToValidate); } } const profile = config.profile?.(params, ctx) ?? defaultProfile(params); const { email } = profile; const secret = params.password as string; let account: GenericDoc; let user: GenericDoc; if (flow === "signUp") { if (secret === undefined) { throw new Error("Missing `password` param for `signUp` flow"); } const created = await createAccount(ctx, { provider, account: { id: email, secret }, profile: profile as any, shouldLinkViaEmail: config.verify !== undefined, shouldLinkViaPhone: false, }); ({ account, user } = created); } else if (flow === "signIn") { if (secret === undefined) { throw new Error("Missing `password` param for `signIn` flow"); } const retrieved = await retrieveAccount(ctx, { provider, account: { id: email, secret }, }); if (retrieved === null) { throw new Error("Invalid credentials"); } ({ account, user } = retrieved); // START: Optional, support password reset } else if (flow === "reset") { if (!config.reset) { throw new Error(`Password reset is not enabled for ${provider}`); } const { account } = await retrieveAccount(ctx, { provider, account: { id: email }, }); return await signInViaProvider(ctx, config.reset, { accountId: account._id, params, }); } else if (flow === "reset-verification") { if (!config.reset) { throw new Error(`Password reset is not enabled for ${provider}`); } if (params.newPassword === undefined) { throw new Error( "Missing `newPassword` param for `reset-verification` flow", ); } const result = await signInViaProvider(ctx, config.reset, { params }); if (result === null) { throw new Error("Invalid code"); } const { userId, sessionId } = result; const secret = params.newPassword as string; await modifyAccountCredentials(ctx, { provider, account: { id: email, secret }, }); await invalidateSessions(ctx, { userId, except: [sessionId] }); return { userId, sessionId }; // END // START: Optional, email verification during sign in } else if (flow === "email-verification") { if (!config.verify) { throw new Error(`Email verification is not enabled for ${provider}`); } const { account } = await retrieveAccount(ctx, { provider, account: { id: email }, }); return await signInViaProvider(ctx, config.verify, { accountId: account._id, params, }); // END } else { throw new Error( "Missing `flow` param, it must be one of " + '"signUp", "signIn", "reset", "reset-verification" or ' + '"email-verification"!', ); } // START: Optional, email verification during sign in if (config.verify && !account.emailVerified) { return await signInViaProvider(ctx, config.verify, { accountId: account._id, params, }); } // END return { userId: user._id }; }, crypto: { async hashSecret(password: string) { return await new Scrypt().hash(password); }, async verifySecret(password: string, hash: string) { return await new Scrypt().verify(hash, password); }, }, extraProviders: [config.reset, config.verify], ...config, }); } function validateDefaultPasswordRequirements(password: string) { if ( password.length < 8 || !/\d/.test(password) || !/[a-z]/.test(password) || !/[A-Z]/.test(password) ) { throw new Error("Invalid password."); } } function defaultProfile(params: Record) { return { email: params.email as string, name: params.name as string, }; }