Skip to content
File

Blob: src/worker/auth/cookies.ts

typescript71 lines
1import { deleteCookie, getCookie, getSignedCookie, setCookie, setSignedCookie } from "hono/cookie";
2import type { CookieOptions } from "hono/utils/cookie";
3 
4import type { AppContext } from "@/worker/routes/hono";
5 
6// Hono's `host` prefix option serializes these logical cookie names with
7// the `__Host-` prefix, forcing Secure + Path=/ + no Domain on the wire.
8// Cloudflare custom domains serve this app over HTTPS, and the test pool
9// runs over a Secure-origin URL, so the prefix is safe to use everywhere.
10export const AUTH_COOKIE_PREFIX = "host";
11export const SESSION_COOKIE_NAME = "goc_session";
12export const OIDC_TX_COOKIE_NAME = "goc_oidc";
13export const SESSION_COOKIE_HEADER_NAME = `__Host-${SESSION_COOKIE_NAME}`;
14export const OIDC_TX_COOKIE_HEADER_NAME = `__Host-${OIDC_TX_COOKIE_NAME}`;
15 
16const SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days
17const OIDC_TX_COOKIE_MAX_AGE_SECONDS = 5 * 60; // matches transaction payload TTL
18 
19const SHARED_COOKIE_OPTIONS = {
20 httpOnly: true,
21 sameSite: "Lax",
22 prefix: AUTH_COOKIE_PREFIX,
23} as const satisfies CookieOptions;
24 
25export type SignedCookieReadResult =
26 | { kind: "ok"; value: string }
27 | { kind: "missing" }
28 | { kind: "invalid_signature" };
29export type CookieSigningSecret = BufferSource;
30 
31export function setSessionCookie(c: AppContext, opaqueToken: string): void {
32 setCookie(c, SESSION_COOKIE_NAME, opaqueToken, {
33 ...SHARED_COOKIE_OPTIONS,
34 maxAge: SESSION_COOKIE_MAX_AGE_SECONDS,
35 });
36}
37 
38export function getSessionCookie(c: AppContext): string | undefined {
39 return getCookie(c, SESSION_COOKIE_NAME, AUTH_COOKIE_PREFIX);
40}
41 
42export function clearSessionCookie(c: AppContext): void {
43 deleteCookie(c, SESSION_COOKIE_NAME, SHARED_COOKIE_OPTIONS);
44}
45 
46export async function setOidcTransactionCookie(
47 c: AppContext,
48 value: string,
49 secret: CookieSigningSecret
50): Promise<void> {
51 await setSignedCookie(c, OIDC_TX_COOKIE_NAME, value, secret, {
52 ...SHARED_COOKIE_OPTIONS,
53 maxAge: OIDC_TX_COOKIE_MAX_AGE_SECONDS,
54 });
55}
56 
57export async function getOidcTransactionCookie(
58 c: AppContext,
59 secret: CookieSigningSecret
60): Promise<SignedCookieReadResult> {
61 const rawValue = getCookie(c, OIDC_TX_COOKIE_NAME, AUTH_COOKIE_PREFIX);
62 if (rawValue === undefined) return { kind: "missing" };
63 const value = await getSignedCookie(c, secret, OIDC_TX_COOKIE_NAME, AUTH_COOKIE_PREFIX);
64 if (value === undefined || value === false) return { kind: "invalid_signature" };
65 return { kind: "ok", value };
66}
67 
68export function clearOidcTransactionCookie(c: AppContext): void {
69 deleteCookie(c, OIDC_TX_COOKIE_NAME, SHARED_COOKIE_OPTIONS);
70}