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:
109
apps/api-server/src/utils/cookies.ts
Normal file
109
apps/api-server/src/utils/cookies.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Cookie configuration for authentication
|
||||
* All cookies use 'rev.' prefix, HttpOnly, Secure, SameSite=Lax
|
||||
*/
|
||||
|
||||
export const COOKIE_NAMES = {
|
||||
SESSION_TOKEN: "rev.session_token",
|
||||
DEVICE_FINGERPRINT: "rev.device_fingerprint",
|
||||
LOGIN_REQUEST_TOKEN: "rev.login_request_token",
|
||||
} as const;
|
||||
|
||||
export const COOKIE_DURATIONS = {
|
||||
SESSION: 60 * 60 * 24 * 7, // 7 days in seconds
|
||||
DEVICE_FINGERPRINT: 60 * 60 * 24 * 365, // 1 year in seconds
|
||||
LOGIN_REQUEST: 60 * 15, // 15 minutes in seconds
|
||||
} as const;
|
||||
|
||||
export const TOKEN_DURATIONS = {
|
||||
EMAIL_VERIFICATION: 60 * 60 * 24, // 24 hours in seconds
|
||||
PASSWORD_RESET: 60 * 60, // 1 hour in seconds
|
||||
} as const;
|
||||
|
||||
// Base cookie options (all cookies share these)
|
||||
const baseCookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax" as const,
|
||||
path: "/",
|
||||
};
|
||||
|
||||
export const COOKIE_OPTIONS = {
|
||||
session: { ...baseCookieOptions, maxAge: COOKIE_DURATIONS.SESSION },
|
||||
device: {
|
||||
...baseCookieOptions,
|
||||
maxAge: COOKIE_DURATIONS.DEVICE_FINGERPRINT,
|
||||
},
|
||||
loginRequest: {
|
||||
...baseCookieOptions,
|
||||
maxAge: COOKIE_DURATIONS.LOGIN_REQUEST,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Cookie options type for setCookie function
|
||||
*/
|
||||
export interface CookieOptions {
|
||||
httpOnly?: boolean;
|
||||
secure?: boolean;
|
||||
sameSite?: "strict" | "lax" | "none";
|
||||
path?: string;
|
||||
maxAge?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse cookie string and get a specific cookie value
|
||||
*/
|
||||
export const getCookie = (
|
||||
headers: Headers,
|
||||
name: string,
|
||||
): string | undefined => {
|
||||
const cookieHeader = headers.get("Cookie");
|
||||
if (!cookieHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cookies = cookieHeader.split(";").map((c) => c.trim());
|
||||
for (const cookie of cookies) {
|
||||
const [cookieName, ...valueParts] = cookie.split("=");
|
||||
if (cookieName === name) {
|
||||
return valueParts.join("=");
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a cookie in the response headers
|
||||
*/
|
||||
export const setCookie = (
|
||||
headers: Headers,
|
||||
name: string,
|
||||
value: string,
|
||||
options: CookieOptions,
|
||||
): void => {
|
||||
const parts = [`${name}=${value}`];
|
||||
if (options.httpOnly) {
|
||||
parts.push("HttpOnly");
|
||||
}
|
||||
if (options.secure) {
|
||||
parts.push("Secure");
|
||||
}
|
||||
if (options.sameSite) {
|
||||
parts.push(`SameSite=${options.sameSite}`);
|
||||
}
|
||||
if (options.path) {
|
||||
parts.push(`Path=${options.path}`);
|
||||
}
|
||||
if (options.maxAge) {
|
||||
parts.push(`Max-Age=${String(options.maxAge)}`);
|
||||
}
|
||||
headers.append("Set-Cookie", parts.join("; "));
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a cookie by setting it to expire immediately
|
||||
*/
|
||||
export const deleteCookie = (headers: Headers, name: string): void => {
|
||||
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
|
||||
};
|
||||
Reference in New Issue
Block a user