forked from Mike/freno-dev
487 lines
15 KiB
TypeScript
487 lines
15 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|