aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkgs/server/src/middleware/auth.test.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-03 05:54:50 +0900
committernsfisis <nsfisis@gmail.com>2025-12-04 23:26:31 +0900
commit4d30f46d4691c9aead411b893b1ab279b05d439c (patch)
treeb5c05015a4b367daf7d9d885f67a7fb1cbc0d0bf /pkgs/server/src/middleware/auth.test.ts
parent0763153865e2157e0d06c946993dd8b235b06c83 (diff)
downloadkioku-4d30f46d4691c9aead411b893b1ab279b05d439c.tar.gz
kioku-4d30f46d4691c9aead411b893b1ab279b05d439c.tar.zst
kioku-4d30f46d4691c9aead411b893b1ab279b05d439c.zip
feat(auth): add auth middleware for JWT validation
Add middleware that validates JWT tokens from Authorization header and sets authenticated user in request context. Includes helper function getAuthUser() to retrieve user from context with proper error handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'pkgs/server/src/middleware/auth.test.ts')
-rw-r--r--pkgs/server/src/middleware/auth.test.ts190
1 files changed, 190 insertions, 0 deletions
diff --git a/pkgs/server/src/middleware/auth.test.ts b/pkgs/server/src/middleware/auth.test.ts
new file mode 100644
index 0000000..8c4286b
--- /dev/null
+++ b/pkgs/server/src/middleware/auth.test.ts
@@ -0,0 +1,190 @@
+import { Hono } from "hono";
+import { sign } from "hono/jwt";
+import { beforeEach, describe, expect, it } from "vitest";
+import { authMiddleware, getAuthUser } from "./auth";
+import { errorHandler } from "./error-handler";
+
+const JWT_SECRET = process.env.JWT_SECRET || "test-secret";
+
+interface ErrorResponse {
+ error: {
+ code: string;
+ message: string;
+ };
+}
+
+interface SuccessResponse {
+ userId: string;
+}
+
+describe("authMiddleware", () => {
+ let app: Hono;
+
+ beforeEach(() => {
+ app = new Hono();
+ app.onError(errorHandler);
+ app.use("/protected/*", authMiddleware);
+ app.get("/protected/resource", (c) => {
+ const user = getAuthUser(c);
+ return c.json({ userId: user.id });
+ });
+ });
+
+ it("allows access with valid token", async () => {
+ const now = Math.floor(Date.now() / 1000);
+ const token = await sign(
+ {
+ sub: "user-123",
+ iat: now,
+ exp: now + 3600,
+ },
+ JWT_SECRET,
+ );
+
+ const res = await app.request("/protected/resource", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as SuccessResponse;
+ expect(body.userId).toBe("user-123");
+ });
+
+ it("returns 401 when Authorization header is missing", async () => {
+ const res = await app.request("/protected/resource");
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("MISSING_AUTH");
+ });
+
+ it("returns 401 for invalid Authorization format", async () => {
+ const res = await app.request("/protected/resource", {
+ headers: {
+ Authorization: "Basic sometoken",
+ },
+ });
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("INVALID_AUTH_FORMAT");
+ });
+
+ it("returns 401 for empty Bearer token", async () => {
+ const res = await app.request("/protected/resource", {
+ headers: {
+ Authorization: "Bearer",
+ },
+ });
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("INVALID_AUTH_FORMAT");
+ });
+
+ it("returns 401 for expired token", async () => {
+ const now = Math.floor(Date.now() / 1000);
+ const token = await sign(
+ {
+ sub: "user-123",
+ iat: now - 7200,
+ exp: now - 3600, // expired 1 hour ago
+ },
+ JWT_SECRET,
+ );
+
+ const res = await app.request("/protected/resource", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("INVALID_TOKEN");
+ });
+
+ it("returns 401 for invalid token", async () => {
+ const res = await app.request("/protected/resource", {
+ headers: {
+ Authorization: "Bearer invalid.token.here",
+ },
+ });
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("INVALID_TOKEN");
+ });
+
+ it("returns 401 for token signed with wrong secret", async () => {
+ const now = Math.floor(Date.now() / 1000);
+ const token = await sign(
+ {
+ sub: "user-123",
+ iat: now,
+ exp: now + 3600,
+ },
+ "wrong-secret",
+ );
+
+ const res = await app.request("/protected/resource", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("INVALID_TOKEN");
+ });
+});
+
+describe("getAuthUser", () => {
+ it("returns user from context when authenticated", async () => {
+ const app = new Hono();
+ app.onError(errorHandler);
+ app.use("/test", authMiddleware);
+ app.get("/test", (c) => {
+ const user = getAuthUser(c);
+ return c.json({ id: user.id });
+ });
+
+ const now = Math.floor(Date.now() / 1000);
+ const token = await sign(
+ {
+ sub: "test-user-456",
+ iat: now,
+ exp: now + 3600,
+ },
+ JWT_SECRET,
+ );
+
+ const res = await app.request("/test", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as { id: string };
+ expect(body.id).toBe("test-user-456");
+ });
+
+ it("throws when user is not in context", async () => {
+ const app = new Hono();
+ app.onError(errorHandler);
+ // Note: no authMiddleware applied
+ app.get("/unprotected", (c) => {
+ const user = getAuthUser(c);
+ return c.json({ id: user.id });
+ });
+
+ const res = await app.request("/unprotected");
+
+ expect(res.status).toBe(401);
+ const body = (await res.json()) as ErrorResponse;
+ expect(body.error.code).toBe("NOT_AUTHENTICATED");
+ });
+});