diff --git a/src/web/middleware/requireAuth.test.ts b/src/web/middleware/requireAuth.test.ts new file mode 100644 index 0000000..644ecb3 --- /dev/null +++ b/src/web/middleware/requireAuth.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import express from "express"; +import cookieParser from "cookie-parser"; +import request from "supertest"; +import { createDatabase, type BotDatabase } from "../../data/database.js"; +import { createUserStore } from "../../data/users.js"; +import { createSessionStore } from "../../data/sessions.js"; +import { createRequireAuth } from "./requireAuth.js"; +import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; + +describe("requireAuth middleware", () => { + let botDb: BotDatabase; + let app: express.Express; + let validToken: string; + + beforeEach(async () => { + botDb = createDatabase(":memory:"); + const users = createUserStore(botDb.db); + const sessions = createSessionStore(botDb.db); + const u = await users.createUser("alice", "pw"); + validToken = sessions.createSession(u.id).token; + + app = express(); + app.use(cookieParser()); + app.use(createRequireAuth(sessions)); + app.get("/protected", (req, res) => { + res.json({ ok: true, user: (req as any).user }); + }); + }); + + afterEach(() => { + botDb.close(); + }); + + it("rejects requests without a session cookie", async () => { + const res = await request(app).get("/protected"); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "unauthenticated" }); + }); + + it("rejects requests with an unknown session cookie", async () => { + const res = await request(app) + .get("/protected") + .set("Cookie", `${SESSION_COOKIE_NAME}=garbage`); + expect(res.status).toBe(401); + }); + + it("allows requests with a valid session cookie and attaches req.user", async () => { + const res = await request(app) + .get("/protected") + .set("Cookie", `${SESSION_COOKIE_NAME}=${validToken}`); + expect(res.status).toBe(200); + expect(res.body.ok).toBe(true); + expect(res.body.user.username).toBe("alice"); + }); +}); diff --git a/src/web/middleware/requireAuth.ts b/src/web/middleware/requireAuth.ts new file mode 100644 index 0000000..e674a28 --- /dev/null +++ b/src/web/middleware/requireAuth.ts @@ -0,0 +1,22 @@ +import type { Request, Response, NextFunction, RequestHandler } from "express"; +import type { SessionStore } from "../../data/sessions.js"; +import { validateSessionFromHeaders, SESSION_COOKIE_NAME } from "../auth/validateSession.js"; + +declare module "express-serve-static-core" { + interface Request { + user?: { id: string; username: string }; + } +} + +export function createRequireAuth(sessions: SessionStore): RequestHandler { + return function requireAuth(req: Request, res: Response, next: NextFunction) { + const result = validateSessionFromHeaders(req.headers.cookie, sessions); + if (!result) { + res.clearCookie(SESSION_COOKIE_NAME, { path: "/" }); + res.status(401).json({ error: "unauthenticated" }); + return; + } + req.user = { id: result.userId, username: result.username }; + next(); + }; +}