Account security flows
These flows are opt-in via useAuth options. They build on the same adapter and add routes under the auth router's mount path (e.g. /api/auth).
CSRF protection
useAuth({ adapter, login, csrf: true });
// or: csrf: { headerName: "X-CSRF-Token", cookieName: "csrf_token", skip: (c) => false }
Uses the double-submit-cookie pattern: a non-httpOnly csrf_token cookie is issued on safe requests and refreshed on login, and unsafe methods (POST/PUT/PATCH/DELETE) must echo it back in the X-CSRF-Token header. Requests carrying an Authorization header (bearer/API-key clients) are exempt. A mismatch returns 403. Also available standalone as createCsrfMiddleware.
Login throttling
useAuth({ adapter, login, throttle: true });
// or: throttle: { maxAttempts: 5, windowMs: 15 * 60 * 1000, store: myRateLimitStore }
Failed logins are counted per email and per IP; once maxAttempts (default 5) is exceeded within the window (default 15 min), /login returns 429 with Retry-After. A successful login resets the counters. Provide a distributed RateLimitStore (e.g. Redis-backed) for multi-instance deployments; the default is in-memory.
Email verification
import { InMemoryVerificationTokenStore } from "covara";
useAuth({
adapter,
login,
verification: {
store: new InMemoryVerificationTokenStore(),
sendToken: async ({ identifier, token, expiresAt }) => sendEmail(identifier, token),
markVerified: async (email) => db.update(users).set({ emailVerified: true }).where(eq(users.email, email)),
ttlMs: 24 * 60 * 60 * 1000, // default 24h
hashTokens: true, // store SHA-256 of the token
},
});
Adds POST /verify/request (issues a one-time token and calls sendToken) and POST /verify/confirm ({ email, token } → calls markVerified).
Password reset
useAuth({
adapter,
login,
passwordReset: {
store: new InMemoryVerificationTokenStore(),
sendToken: async ({ identifier, token }) => sendEmail(identifier, token),
resetPassword: async (email, passwordHash) =>
db.update(users).set({ passwordHash }).where(eq(users.email, email)),
findUserByEmail: async (email) => db.query.users.findFirst({ where: eq(users.email, email) }),
ttlMs: 60 * 60 * 1000, // default 1h
logoutEverywhere: true, // revoke all sessions after reset
},
});
Adds:
POST /password/forgot—{ email }→ issues a token viasendToken; always returns{ success: true }(no email enumeration).POST /password/reset—{ email, token, password }→ hashes the new password with the built-in scrypt hasher and callsresetPassword. WhenlogoutEverywhereis set and the adapter implementsinvalidateUserSessions, all of the user's sessions are revoked.
Token stores
verification, passwordReset, and magicLink all use the VerificationTokenStore interface (create/consume/deleteByIdentifier). InMemoryVerificationTokenStore ships for development; back it with your own table for production. Set hashTokens: true to store only the SHA-256 of each token.