Skip to content
File

Blob: src/worker/auth/cookies.ts

typescript99 lines
1import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
2 
3import { base64UrlDecode, base64UrlEncode, utf8Bytes, utf8String } from "@/worker/auth/bytes";
4import { deriveHmacKeyBytes } from "@/worker/auth/crypto";
5import type { AppContext } from "@/worker/types";
6 
7const oidcCookieName = "dab_oidc_tx";
8const sessionCookieName = "dab_session";
9const cookieInfo = "cookie-signing";
10const oidcSalt = "dab:oidc-transaction:v1";
11const sessionSalt = "dab:api-session:v1";
12 
13export const oidcTransactionTtlMs = 10 * 60 * 1000;
14export const apiSessionTtlMs = 6 * 60 * 60 * 1000;
15 
16export interface OidcTransactionCookie {
17 state: string;
18 nonce: string;
19 codeVerifier: string;
20 returnTo: string;
21 createdAtMs: number;
22 expiresAtMs: number;
23}
24 
25export interface SessionCookie {
26 subjectId: string;
27 storageId: string;
28 sessionId: string;
29 createdAtMs: number;
30 expiresAtMs: number;
31}
32 
33async function cookieSecret(secret: string, salt: string): Promise<Uint8Array> {
34 return await deriveHmacKeyBytes(secret, salt, cookieInfo);
35}
36 
37function decodePayload<T>(value: string | false | undefined): T | null {
38 if (!value) return null;
39 try {
40 return JSON.parse(utf8String(base64UrlDecode(value))) as T;
41 } catch {
42 return null;
43 }
44}
45 
46const hostCookieOptions = {
47 prefix: "host",
48 httpOnly: true,
49 secure: true,
50 sameSite: "Lax",
51 path: "/",
52} as const;
53 
54export async function setOidcTransactionCookie(c: AppContext, transaction: OidcTransactionCookie): Promise<void> {
55 const value = base64UrlEncode(utf8Bytes(JSON.stringify(transaction)));
56 await setSignedCookie(c, oidcCookieName, value, await cookieSecret(c.env.TESSERA_OIDC_CLIENT_SECRET, oidcSalt), {
57 ...hostCookieOptions,
58 maxAge: Math.floor(oidcTransactionTtlMs / 1000),
59 expires: new Date(transaction.expiresAtMs),
60 });
61}
62 
63export async function getOidcTransactionCookie(c: AppContext): Promise<OidcTransactionCookie | null> {
64 const value = await getSignedCookie(
65 c,
66 await cookieSecret(c.env.TESSERA_OIDC_CLIENT_SECRET, oidcSalt),
67 oidcCookieName,
68 "host",
69 );
70 return decodePayload<OidcTransactionCookie>(value);
71}
72 
73export function clearOidcTransactionCookie(c: AppContext): void {
74 deleteCookie(c, oidcCookieName, { prefix: "host", path: "/" });
75}
76 
77export async function setSessionCookie(c: AppContext, session: SessionCookie): Promise<void> {
78 const value = base64UrlEncode(utf8Bytes(JSON.stringify(session)));
79 await setSignedCookie(c, sessionCookieName, value, await cookieSecret(c.env.DAB_SESSION_SECRET, sessionSalt), {
80 ...hostCookieOptions,
81 maxAge: Math.floor((session.expiresAtMs - session.createdAtMs) / 1000),
82 expires: new Date(session.expiresAtMs),
83 });
84}
85 
86export async function getSessionCookie(c: AppContext): Promise<SessionCookie | null> {
87 const value = await getSignedCookie(
88 c,
89 await cookieSecret(c.env.DAB_SESSION_SECRET, sessionSalt),
90 sessionCookieName,
91 "host",
92 );
93 return decodePayload<SessionCookie>(value);
94}
95 
96export function clearSessionCookie(c: AppContext): void {
97 deleteCookie(c, sessionCookieName, { prefix: "host", path: "/" });
98}