Skip to content
File

Blob: src/worker/lib/auth.ts

typescript92 lines
1import { SignJWT, jwtVerify } from "jose";
2import { ulid } from "ulid";
3import type { Context } from "hono";
4import { deleteCookie, setCookie } from "hono/cookie";
5import type { CookieOptions } from "hono/utils/cookie";
6 
7import { SESSION_HINT_COOKIE } from "@/shared/auth";
8import { users } from "@/worker/db/d1/schema";
9import { JWT_ALGORITHM, REFRESH_COOKIE_MAX_AGE } from "@/worker/lib/constants";
10 
11export const REFRESH_COOKIE = "bland_refresh";
12 
13const BASE_COOKIE_OPTIONS = {
14 path: "/",
15 sameSite: "Strict",
16 secure: true,
17} satisfies CookieOptions;
18const REFRESH_COOKIE_OPTIONS = {
19 ...BASE_COOKIE_OPTIONS,
20 httpOnly: true,
21 maxAge: REFRESH_COOKIE_MAX_AGE,
22} satisfies CookieOptions;
23const SESSION_HINT_COOKIE_OPTIONS = {
24 ...BASE_COOKIE_OPTIONS,
25 maxAge: REFRESH_COOKIE_MAX_AGE,
26} satisfies CookieOptions;
27 
28export function getJwtSecret(env: Env): Uint8Array {
29 return new TextEncoder().encode(env.JWT_SECRET);
30}
31 
32export async function verifyAccessToken(token: string, env: Env): Promise<{ sub: string; jti: string }> {
33 const { payload } = await jwtVerify(token, getJwtSecret(env), {
34 algorithms: [JWT_ALGORITHM],
35 });
36 
37 if (!payload.sub || !payload.jti) {
38 throw new Error("missing_claims");
39 }
40 
41 if (payload.type === "refresh") {
42 throw new Error("refresh_token_misuse");
43 }
44 
45 return { sub: payload.sub, jti: payload.jti };
46}
47 
48export function generateSecureToken(): string {
49 const bytes = crypto.getRandomValues(new Uint8Array(32));
50 let binary = "";
51 for (let i = 0; i < bytes.length; i++) {
52 binary += String.fromCharCode(bytes[i]);
53 }
54 return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
55}
56 
57export async function createAccessToken(userId: string, env: Env): Promise<string> {
58 return new SignJWT({ sub: userId, jti: ulid() })
59 .setProtectedHeader({ alg: JWT_ALGORITHM })
60 .setIssuedAt()
61 .setExpirationTime("15m")
62 .sign(getJwtSecret(env));
63}
64 
65export async function createRefreshToken(userId: string, env: Env): Promise<string> {
66 return new SignJWT({ sub: userId, jti: ulid(), type: "refresh" })
67 .setProtectedHeader({ alg: JWT_ALGORITHM })
68 .setIssuedAt()
69 .setExpirationTime("7d")
70 .sign(getJwtSecret(env));
71}
72 
73export function setRefreshCookie(c: Context, token: string): void {
74 setCookie(c, REFRESH_COOKIE, token, REFRESH_COOKIE_OPTIONS);
75 setCookie(c, SESSION_HINT_COOKIE, "1", SESSION_HINT_COOKIE_OPTIONS);
76}
77 
78export function clearRefreshCookie(c: Context): void {
79 deleteCookie(c, REFRESH_COOKIE, { ...BASE_COOKIE_OPTIONS, httpOnly: true });
80 deleteCookie(c, SESSION_HINT_COOKIE, BASE_COOKIE_OPTIONS);
81}
82 
83export function toUserResponse(user: typeof users.$inferSelect) {
84 return {
85 id: user.id,
86 email: user.email,
87 name: user.name,
88 avatar_url: user.avatar_url,
89 created_at: user.created_at,
90 };
91}