Implement auth procedures with code review fixes

Add complete auth backend (Workstream D):
- Auth middleware for session/API key authentication
- Signup with password or passkey (WebAuthn)
- Login flow with device trust and email confirmation
- Password reset and email verification
- Session management and logout

Utilities created:
- cookies.ts: Cookie helpers and configuration
- crypto.ts: Token generation and hashing
- password.ts: zxcvbn validation, argon2id hashing
- geo.ts: IP/location extraction from headers
- email.ts: Stubbed email sending
- session.ts: Session creation and device trust

Code review improvements applied:
- Use ORPCError instead of Error in procedures
- Add ast-grep rule to enforce ORPCError usage
- Remove error info leakage (generic messages)
- Optimize N+1 query with JOIN in login-password
- Extract signupWithPassword/signupWithPasskey for testability
- Add 15-minute WebAuthn challenge expiry check
- Strengthen CookieOptions type definitions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 15:19:15 +08:00
parent 8de88472b1
commit 829d365e80
24 changed files with 1739 additions and 47 deletions

View File

@@ -0,0 +1,62 @@
import type { APIContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import { sendPasswordResetEmail } from "../../utils/email.js";
const os = implement(contract);
/**
* Forgot password handler
* Public procedure (no authentication required)
*
* Anti-enumeration: Always returns success even if user doesn't exist
* This prevents attackers from determining which emails are registered
*/
export const forgotPassword = os.auth.forgotPassword.handler(
async ({ input, context }) => {
const ctx = context as APIContext;
const { email } = input;
// Normalize email to lowercase
const normalizedEmail = email.toLowerCase();
// Look up user by email
const user = await ctx.db
.selectFrom("users")
.select(["id", "email"])
.where("email", "=", normalizedEmail)
.executeTakeFirst();
// If user exists, create password reset token and send email
if (user) {
// Delete any existing password reset tokens for this user (security measure)
await ctx.db
.deleteFrom("password_resets")
.where("user_id", "=", user.id)
.execute();
// Generate secure token (64 hex chars)
const token = generateSecureToken();
// Create password reset record with 1 hour expiry
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);
await ctx.db
.insertInto("password_resets")
.values({
user_id: user.id,
token,
expires_at: expiresAt,
})
.execute();
// Send password reset email (stubbed)
await sendPasswordResetEmail(user.email, token);
}
// Always return success (anti-enumeration)
// Don't reveal whether the email exists or not
},
);