import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { createSession } from "~/server/auth"; import { enforceHourlyLimit, enforceRateLimit, HOURLY, requestIp } from "~/server/limits"; import { badRequest, unauthorized } from "~/server/errors"; import { completePasswordReset, confirmEmailVerification, notifySecurityEvent, requestPasswordReset, } from "~/server/account-security"; import { attemptPendingLogin, clearPendingLogin, clearTotpFailures, peekPendingLogin, requiresSecondFactor, secondFactorMethods, useRecoveryCode, verifyUserTotp, } from "~/server/two-factor"; import { passkeyRequestOptions, verifyPasskeyAssertion } from "~/server/passkeys"; import { email, password } from "~/lib/validation"; const encoded = z.string().min(1).max(16384); const assertion = z.object({ challenge: z.string().min(1).max(128), credentialId: z.string().min(1).max(1400), clientDataJSON: encoded, authenticatorData: encoded, signature: encoded, }); async function requirePending(): Promise { const userId = await attemptPendingLogin(); if (!userId) throw unauthorized("Your sign-in expired; enter your password again"); return userId; } export const getTwoFactorChallenge = createServerFn({ method: "GET" }).handler(async () => { const userId = await peekPendingLogin(); if (!userId) return { pending: false, totp: false, passkey: false }; return { pending: true, ...(await secondFactorMethods(userId)) }; }); export const verifyTwoFactorCode = createServerFn({ method: "POST" }) .validator( z.object({ code: z.string().trim().min(6).max(32), kind: z.enum(["totp", "recovery"]), }), ) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `2fa:${requestIp()}`); await enforceHourlyLimit(`2fa:${requestIp()}`, HOURLY.twoFactorPerIp); const userId = await requirePending(); await enforceRateLimit("AUTH_LIMITER", `2fa:user:${userId}`); await enforceHourlyLimit(`2fa:user:${userId}`, HOURLY.twoFactorPerUser); const ok = data.kind === "totp" ? await verifyUserTotp(userId, data.code) : await useRecoveryCode(userId, data.code); if (!ok) throw badRequest(data.kind === "totp" ? "That code is not valid" : "That recovery code is not valid"); if (data.kind === "recovery") await notifySecurityEvent(userId, "a recovery code was used to sign in"); await clearPendingLogin(); await createSession(userId); return { ok: true }; }); export const startTwoFactorPasskey = createServerFn({ method: "POST" }).handler(async () => { await enforceRateLimit("AUTH_LIMITER", `2fa:${requestIp()}`); await enforceHourlyLimit(`2fa:${requestIp()}`, HOURLY.twoFactorPerIp); const userId = await peekPendingLogin(); if (!userId) throw unauthorized("Your sign-in expired; enter your password again"); return passkeyRequestOptions(userId); }); export const finishTwoFactorPasskey = createServerFn({ method: "POST" }) .validator(assertion) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `2fa:${requestIp()}`); await enforceHourlyLimit(`2fa:${requestIp()}`, HOURLY.twoFactorPerIp); const userId = await requirePending(); await enforceHourlyLimit(`2fa:user:${userId}`, HOURLY.twoFactorPerUser); await verifyPasskeyAssertion(data, userId); await clearTotpFailures(userId); await clearPendingLogin(); await createSession(userId); return { ok: true }; }); export const startPasskeyLogin = createServerFn({ method: "POST" }).handler(async () => { await enforceRateLimit("AUTH_LIMITER", `passkey:${requestIp()}`); return passkeyRequestOptions(null); }); /** Passwordless sign-in: a user-verified passkey satisfies both factors. */ export const finishPasskeyLogin = createServerFn({ method: "POST" }) .validator(assertion) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `passkey:${requestIp()}`); const userId = await verifyPasskeyAssertion(data, null); await clearTotpFailures(userId); await createSession(userId); return { ok: true }; }); export const requestPasswordResetEmail = createServerFn({ method: "POST" }) .validator(z.object({ email })) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `reset:${requestIp()}`); await enforceRateLimit("AUTH_LIMITER", `reset:${data.email.toLowerCase()}`); await requestPasswordReset(data.email); return { ok: true }; }); export const resetPassword = createServerFn({ method: "POST" }) .validator(z.object({ token: z.string().min(1).max(200), password })) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `reset:${requestIp()}`); const userId = await completePasswordReset(data.token, data.password); if (await requiresSecondFactor(userId)) return { ok: true, signedIn: false }; await createSession(userId); return { ok: true, signedIn: true }; }); export const verifyEmail = createServerFn({ method: "POST" }) .validator(z.object({ token: z.string().min(1).max(200) })) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `verify:${requestIp()}`); const result = await confirmEmailVerification(data.token); return { ok: true, email: result.email }; });