Spaces:
Running
Running
File size: 4,366 Bytes
d579c21 ce4b4fb 82b44e7 ce4b4fb d579c21 523ba8d ce4b4fb d579c21 ce4b4fb 82b44e7 ce4b4fb 35243f1 ce4b4fb 82b44e7 35243f1 ce4b4fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | import { createHmac, timingSafeEqual } from "node:crypto";
import type { Request, Response } from "express";
import type { Session, User } from "@supabase/supabase-js";
import { env, isProduction } from "./config.js";
import { supabaseAuth } from "./supabase.js";
import { singleFlight } from "./single-flight.js";
// Several page, image and polling requests arrive together. Validate once for
// that burst, without extending a revoked token's lifetime through a TTL cache.
const verifyAccess = singleFlight((token: string) => supabaseAuth.auth.getUser(token));
const refreshAccess = singleFlight((token: string) => supabaseAuth.auth.refreshSession({ refresh_token: token }));
const accessCookie = "sp_access";
const refreshCookie = "sp_refresh";
const passwordRecoveryCookie = "sp_password_recovery";
const passwordRecoveryWindowMs = 15 * 60 * 1000;
const sameSite = "lax" as const;
const cookieOptions = { httpOnly: true, secure: isProduction, sameSite, path: "/" };
const passwordRecoveryCookieOptions = {
httpOnly: true, secure: isProduction, sameSite, path: "/api/auth/password/recovery",
};
function passwordRecoverySignature(userId: string, expiresAt: number) {
return createHmac("sha256", env.SUPABASE_SERVICE_ROLE_KEY)
.update(`sporalize-password-recovery:${userId}:${expiresAt}`)
.digest("base64url");
}
export function setPasswordRecoveryCookie(res: Response, userId: string) {
const expiresAt = Date.now() + passwordRecoveryWindowMs;
res.cookie(passwordRecoveryCookie, `${expiresAt}.${passwordRecoverySignature(userId, expiresAt)}`, {
...passwordRecoveryCookieOptions, maxAge: passwordRecoveryWindowMs,
});
}
export function hasPasswordRecoveryCookie(req: Request, userId: string) {
const value = req.cookies?.[passwordRecoveryCookie];
if (typeof value !== "string") return false;
const [expiresText, suppliedSignature, extra] = value.split(".");
const expiresAt = Number(expiresText);
if (extra !== undefined || !Number.isSafeInteger(expiresAt) || expiresAt <= Date.now() ||
expiresAt > Date.now() + passwordRecoveryWindowMs + 5_000 || !suppliedSignature) return false;
const expected = Buffer.from(passwordRecoverySignature(userId, expiresAt));
const supplied = Buffer.from(suppliedSignature);
return expected.length === supplied.length && timingSafeEqual(expected, supplied);
}
export function clearPasswordRecoveryCookie(res: Response) {
res.clearCookie(passwordRecoveryCookie, passwordRecoveryCookieOptions);
}
export function setSessionCookies(res: Response, session: Session) {
res.cookie(accessCookie, session.access_token, { ...cookieOptions, maxAge: Math.max(session.expires_in * 1000, 60_000) });
res.cookie(refreshCookie, session.refresh_token, { ...cookieOptions, maxAge: 1000 * 60 * 60 * 24 * 30 });
}
export function clearSessionCookies(res: Response) {
res.clearCookie(accessCookie, cookieOptions);
res.clearCookie(refreshCookie, cookieOptions);
}
export async function authenticatedUser(req: Request, res: Response): Promise<{ user: User; session?: Session } | null> {
const accessToken = req.cookies?.[accessCookie] as string | undefined;
const refreshToken = req.cookies?.[refreshCookie] as string | undefined;
if (!accessToken && !refreshToken) return null;
if (accessToken) {
const { data, error } = await verifyAccess(accessToken);
if (data.user && !error) return { user: data.user };
// An outage is not evidence that credentials expired. Do not rotate or clear cookies.
if (error && ![400, 401, 403].includes(error.status ?? 0)) throw error;
}
if (!refreshToken) {
clearSessionCookies(res);
return null;
}
const { data, error } = await refreshAccess(refreshToken);
if (error && ![400, 401, 403].includes(error.status ?? 0)) throw error;
if (error || !data.user || !data.session) {
clearSessionCookies(res);
return null;
}
setSessionCookies(res, data.session);
return { user: data.user, session: data.session };
}
export function safeRedirect(path: unknown) {
const frontend = new URL(env.FRONTEND_URL);
const fallback = new URL("/labs", frontend).toString();
if (typeof path !== "string" || !path.startsWith("/")) return fallback;
try {
const target = new URL(path, frontend);
return target.origin === frontend.origin ? target.toString() : fallback;
} catch {
return fallback;
}
}
|