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,38 @@
import { createHash, randomBytes } from "node:crypto";
/**
* Hash a token with SHA-256 for storage in database
* Never store raw tokens - always hash first
*/
export const hashToken = (token: string): string => {
return createHash("sha256").update(token).digest("hex");
};
/**
* Generate a session token (UUID v4)
*/
export const generateSessionToken = (): string => {
return crypto.randomUUID();
};
/**
* Generate a device fingerprint (UUID v4)
*/
export const generateDeviceFingerprint = (): string => {
return crypto.randomUUID();
};
/**
* Generate a secure random token for email verification, password reset, etc.
* Uses 32 bytes (256 bits) of entropy
*/
export const generateSecureToken = (): string => {
return randomBytes(32).toString("hex");
};
/**
* Generate expiration date
*/
export const generateExpiry = (seconds: number): Date => {
return new Date(Date.now() + seconds * 1000);
};