forked from Mike/freno-dev
security hardening
This commit is contained in:
486
src/server/security/auth.test.ts
Normal file
486
src/server/security/auth.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user