security hardening

This commit is contained in:
2025-12-28 20:04:29 -05:00
parent aefd467660
commit 1ba20339a8
22 changed files with 5177 additions and 116 deletions

View File

@@ -0,0 +1,486 @@
/**
* Authentication Security Tests
* Tests for authentication mechanisms including JWT, session management, and timing attacks
*/
import { describe, it, expect, beforeEach } from "bun:test";
import { getUserID, getPrivilegeLevel, checkAuthStatus } from "~/server/auth";
import {
createMockEvent,
createTestJWT,
createExpiredJWT,
createInvalidSignatureJWT,
measureTime
} from "./test-utils";
import { jwtVerify, SignJWT } from "jose";
import { env } from "~/env/server";
describe("Authentication Security", () => {
describe("JWT Token Validation", () => {
it("should validate correct JWT tokens", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBe(userId);
});
it("should reject expired JWT tokens", async () => {
const userId = "test-user-123";
const expiredToken = await createExpiredJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: expiredToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject JWT tokens with invalid signature", async () => {
const userId = "test-user-123";
const invalidToken = await createInvalidSignatureJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: invalidToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject malformed JWT tokens", async () => {
const event = createMockEvent({
cookies: { userIDToken: "not-a-valid-jwt" }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject empty JWT tokens", async () => {
const event = createMockEvent({
cookies: { userIDToken: "" }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject JWT tokens with missing user ID", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const tokenWithoutId = await new SignJWT({})
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: tokenWithoutId }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject JWT tokens with invalid user ID type", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const tokenWithNumberId = await new SignJWT({ id: 12345 })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: tokenWithNumberId }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should handle missing cookie gracefully", async () => {
const event = createMockEvent({});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
});
describe("JWT Token Tampering", () => {
it("should detect modified JWT payload", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
// Tamper with the payload (middle part of JWT)
const parts = token.split(".");
const tamperedPayload = Buffer.from(
JSON.stringify({ id: "attacker-id" })
).toString("base64url");
const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
const event = createMockEvent({
cookies: { userIDToken: tamperedToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should detect modified JWT signature", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
// Tamper with the signature (last part of JWT)
const parts = token.split(".");
const tamperedToken = `${parts[0]}.${parts[1]}.modified-signature`;
const event = createMockEvent({
cookies: { userIDToken: tamperedToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject none algorithm JWT tokens", async () => {
// Try to create a token with 'none' algorithm (security vulnerability)
const payload = Buffer.from(
JSON.stringify({ id: "attacker-id", exp: Date.now() / 1000 + 3600 })
).toString("base64url");
const header = Buffer.from(
JSON.stringify({ alg: "none", typ: "JWT" })
).toString("base64url");
const noneToken = `${header}.${payload}.`;
const event = createMockEvent({
cookies: { userIDToken: noneToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
});
describe("Privilege Level Security", () => {
it("should return admin privilege for admin user", async () => {
const adminId = env.ADMIN_ID;
const token = await createTestJWT(adminId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("admin");
});
it("should return user privilege for regular user", async () => {
const userId = "regular-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("user");
});
it("should return anonymous privilege for unauthenticated request", async () => {
const event = createMockEvent({});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should return anonymous privilege for invalid token", async () => {
const event = createMockEvent({
cookies: { userIDToken: "invalid-token" }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should not allow privilege escalation through token manipulation", async () => {
const userId = "regular-user-123";
const token = await createTestJWT(userId);
// Even if attacker modifies the token, signature verification will fail
const parts = token.split(".");
const fakeAdminPayload = Buffer.from(
JSON.stringify({ id: env.ADMIN_ID })
).toString("base64url");
const fakeAdminToken = `${parts[0]}.${fakeAdminPayload}.${parts[2]}`;
const event = createMockEvent({
cookies: { userIDToken: fakeAdminToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous"); // Token validation fails
});
});
describe("Session Management", () => {
it("should identify authenticated sessions correctly", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const authStatus = await checkAuthStatus(event);
expect(authStatus.isAuthenticated).toBe(true);
expect(authStatus.userId).toBe(userId);
});
it("should identify unauthenticated sessions correctly", async () => {
const event = createMockEvent({});
const authStatus = await checkAuthStatus(event);
expect(authStatus.isAuthenticated).toBe(false);
expect(authStatus.userId).toBeNull();
});
it("should handle session with expired token", async () => {
const userId = "test-user-123";
const expiredToken = await createExpiredJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: expiredToken }
});
const authStatus = await checkAuthStatus(event);
expect(authStatus.isAuthenticated).toBe(false);
expect(authStatus.userId).toBeNull();
});
});
describe("Timing Attack Prevention", () => {
it("should have consistent timing for valid and invalid tokens", async () => {
const userId = "test-user-123";
const validToken = await createTestJWT(userId);
const invalidToken = "invalid-token";
// Measure time for valid token
const validEvent = createMockEvent({
cookies: { userIDToken: validToken }
});
const { duration: validDuration } = await measureTime(() =>
getUserID(validEvent)
);
// Measure time for invalid token
const invalidEvent = createMockEvent({
cookies: { userIDToken: invalidToken }
});
const { duration: invalidDuration } = await measureTime(() =>
getUserID(invalidEvent)
);
// Timing difference should be minimal (within reasonable variance)
// This helps prevent timing attacks to enumerate valid tokens
const timingDifference = Math.abs(validDuration - invalidDuration);
// Allow up to 5ms variance (accounts for system variations)
expect(timingDifference).toBeLessThan(5);
});
it("should have consistent timing for different user privilege levels", async () => {
const adminId = env.ADMIN_ID;
const userId = "regular-user-123";
const adminToken = await createTestJWT(adminId);
const userToken = await createTestJWT(userId);
// Measure time for admin privilege check
const adminEvent = createMockEvent({
cookies: { userIDToken: adminToken }
});
const { duration: adminDuration } = await measureTime(() =>
getPrivilegeLevel(adminEvent)
);
// Measure time for user privilege check
const userEvent = createMockEvent({
cookies: { userIDToken: userToken }
});
const { duration: userDuration } = await measureTime(() =>
getPrivilegeLevel(userEvent)
);
const timingDifference = Math.abs(adminDuration - userDuration);
expect(timingDifference).toBeLessThan(5);
});
});
describe("Token Expiration", () => {
it("should respect token expiration time", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const userId = "test-user-123";
// Create token expiring in 1 second
const shortLivedToken = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1s")
.sign(secret);
// Should work immediately
const event1 = createMockEvent({
cookies: { userIDToken: shortLivedToken }
});
const id1 = await getUserID(event1);
expect(id1).toBe(userId);
// Wait for token to expire
await new Promise((resolve) => setTimeout(resolve, 1500));
// Should fail after expiration
const event2 = createMockEvent({
cookies: { userIDToken: shortLivedToken }
});
const id2 = await getUserID(event2);
expect(id2).toBeNull();
});
it("should handle tokens with very long expiration", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const userId = "test-user-123";
const longLivedToken = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("365d") // 1 year
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: longLivedToken }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe(userId);
});
it("should reject tokens with past expiration timestamps", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const userId = "test-user-123";
const pastToken = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(Math.floor(Date.now() / 1000) - 3600) // 1 hour ago
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: pastToken }
});
const extractedId = await getUserID(event);
expect(extractedId).toBeNull();
});
});
describe("Edge Cases", () => {
it("should handle very long JWT tokens", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const largePayload = {
id: "test-user-123",
extraData: "x".repeat(10000) // 10KB of extra data
};
const largeToken = await new SignJWT(largePayload)
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: largeToken }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe("test-user-123");
});
it("should handle special characters in user IDs", async () => {
const specialUserId = "user-with-special-!@#$%^&*()";
const token = await createTestJWT(specialUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe(specialUserId);
});
it("should handle unicode user IDs", async () => {
const unicodeUserId = "user-with-unicode-🔐🛡️";
const token = await createTestJWT(unicodeUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe(unicodeUserId);
});
it("should reject JWT with future issued-at time", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const futureToken = await new SignJWT({ id: "test-user-123" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt(Math.floor(Date.now() / 1000) + 3600) // 1 hour in future
.setExpirationTime("2h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: futureToken }
});
// Some JWT libraries reject future iat, some don't
// This test documents the behavior
const extractedId = await getUserID(event);
// Behavior may vary - just ensure no crash
expect(extractedId === null || extractedId === "test-user-123").toBe(
true
);
});
});
describe("Performance", () => {
it("should validate tokens efficiently", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const start = performance.now();
for (let i = 0; i < 1000; i++) {
await getUserID(event);
}
const duration = performance.now() - start;
// Should validate 1000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
it("should check privilege levels efficiently", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const start = performance.now();
for (let i = 0; i < 1000; i++) {
await getPrivilegeLevel(event);
}
const duration = performance.now() - start;
// Should check 1000 privileges in less than 100ms
expect(duration).toBeLessThan(100);
});
});
});