test: enhance security module with comprehensive token validation and rate limiting tests

- Expanded TokenManager test suite with advanced token encryption and decryption scenarios
- Added detailed rate limiting tests with IP-based tracking and window-based expiration
- Improved test coverage for token validation, tampering detection, and error handling
- Implemented mock configurations for faster test execution
- Enhanced security test scenarios with unique IP addresses and edge case handling
This commit is contained in:
jango-blockchained
2025-02-04 04:09:40 +01:00
parent 1e3bf07547
commit 08e408d68d
7 changed files with 318 additions and 511 deletions

View File

@@ -1,37 +1,50 @@
import { describe, expect, it, beforeEach } from "bun:test";
import { TokenManager } from "../index.js";
import { TokenManager } from "../index";
import jwt from "jsonwebtoken";
const validSecret = "test-secret-key-that-is-at-least-32-chars";
const validToken = "valid-token-that-is-at-least-32-characters-long";
const validSecret = "test_secret_that_is_at_least_32_chars_long";
const testIp = "127.0.0.1";
// Mock the rate limit window for faster tests
const MOCK_RATE_LIMIT_WINDOW = 100; // 100ms instead of 15 minutes
describe("Security Module", () => {
beforeEach(() => {
process.env.JWT_SECRET = validSecret;
// Clear any existing rate limit data
// Reset failed attempts map
(TokenManager as any).failedAttempts = new Map();
// Mock the security config
(TokenManager as any).SECURITY_CONFIG = {
...(TokenManager as any).SECURITY_CONFIG,
LOCKOUT_DURATION: MOCK_RATE_LIMIT_WINDOW,
MAX_FAILED_ATTEMPTS: 5,
MAX_TOKEN_AGE: 30 * 24 * 60 * 60 * 1000 // 30 days
};
});
describe("TokenManager", () => {
it("should encrypt and decrypt tokens", () => {
const encrypted = TokenManager.encryptToken(validToken, validSecret);
expect(encrypted).toBeDefined();
expect(typeof encrypted).toBe("string");
expect(encrypted === validToken).toBe(false);
const originalToken = "test-token";
const encryptedToken = TokenManager.encryptToken(originalToken, validSecret);
expect(encryptedToken).toBeDefined();
expect(encryptedToken.includes(originalToken)).toBe(false);
const decrypted = TokenManager.decryptToken(encrypted, validSecret);
expect(decrypted).toBe(validToken);
const decryptedToken = TokenManager.decryptToken(encryptedToken, validSecret);
expect(decryptedToken).toBeDefined();
expect(decryptedToken).toBe(originalToken);
});
it("should validate tokens correctly", () => {
const payload = { userId: "123", role: "user" };
const token = jwt.sign(payload, validSecret, { expiresIn: "1h" });
expect(token).toBeDefined();
const result = TokenManager.validateToken(token, testIp);
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
// Verify payload separately
const decoded = jwt.verify(token, validSecret) as typeof payload;
expect(decoded.userId).toBe(payload.userId);
expect(decoded.role).toBe(payload.role);
});
it("should handle empty tokens", () => {
@@ -53,66 +66,119 @@ describe("Security Module", () => {
expect(result.valid).toBe(false);
expect(result.error).toBe("Token has expired");
});
});
describe("Request Validation", () => {
it("should validate requests with valid tokens", () => {
it("should handle token tampering", () => {
// Use a different IP for this test to avoid rate limiting
const uniqueIp = "192.168.1.1";
const payload = { userId: "123", role: "user" };
const token = jwt.sign(payload, validSecret, { expiresIn: "1h" });
const result = TokenManager.validateToken(token, testIp);
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
});
const token = jwt.sign(payload, validSecret);
const tamperedToken = token.slice(0, -5) + "xxxxx"; // Tamper with signature
it("should reject invalid tokens", () => {
const result = TokenManager.validateToken("invalid-token", testIp);
const result = TokenManager.validateToken(tamperedToken, uniqueIp);
expect(result.valid).toBe(false);
expect(result.error).toBe("Token length below minimum requirement");
expect(result.error).toBe("Invalid token signature");
});
});
describe("Error Handling", () => {
it("should handle missing JWT secret", () => {
delete process.env.JWT_SECRET;
const payload = { userId: "123", role: "user" };
const result = TokenManager.validateToken(jwt.sign(payload, "some-secret"), testIp);
expect(result.valid).toBe(false);
expect(result.error).toBe("JWT secret not configured");
describe("Token Encryption", () => {
it("should use different IVs for same token", () => {
const token = "test-token";
const encrypted1 = TokenManager.encryptToken(token, validSecret);
const encrypted2 = TokenManager.encryptToken(token, validSecret);
expect(encrypted1).toBeDefined();
expect(encrypted2).toBeDefined();
expect(encrypted1 === encrypted2).toBe(false);
});
it("should handle invalid token format", () => {
const result = TokenManager.validateToken("not-a-jwt-token", testIp);
expect(result.valid).toBe(false);
expect(result.error).toBe("Token length below minimum requirement");
it("should handle large tokens", () => {
const largeToken = "x".repeat(1024);
const encrypted = TokenManager.encryptToken(largeToken, validSecret);
const decrypted = TokenManager.decryptToken(encrypted, validSecret);
expect(decrypted).toBe(largeToken);
});
it("should handle encryption errors", () => {
expect(() => TokenManager.encryptToken("", validSecret)).toThrow("Invalid token");
expect(() => TokenManager.encryptToken(validToken, "short-key")).toThrow("Invalid encryption key");
});
it("should handle decryption errors", () => {
expect(() => TokenManager.decryptToken("invalid:format", validSecret)).toThrow();
expect(() => TokenManager.decryptToken("aes-256-gcm:invalid:base64:data", validSecret)).toThrow();
it("should fail gracefully with invalid encrypted data", () => {
expect(() => TokenManager.decryptToken("invalid-encrypted-data", validSecret))
.toThrow("Invalid encrypted token");
});
});
describe("Rate Limiting", () => {
it("should implement rate limiting for failed attempts", () => {
// Create an invalid token that's long enough to pass length check
const invalidToken = "x".repeat(64); // Long enough to pass MIN_TOKEN_LENGTH check
beforeEach(() => {
// Reset failed attempts before each test
(TokenManager as any).failedAttempts = new Map();
});
// First attempt should fail with token validation error and record the attempt
const firstResult = TokenManager.validateToken(invalidToken, testIp);
expect(firstResult.valid).toBe(false);
expect(firstResult.error).toBe("Too many failed attempts. Please try again later.");
it("should track failed attempts by IP", () => {
const invalidToken = "x".repeat(64);
const ip1 = "1.1.1.1";
const ip2 = "2.2.2.2";
// Verify that even a valid token is blocked during rate limiting
const validPayload = { userId: "123", role: "user" };
const validToken = jwt.sign(validPayload, validSecret, { expiresIn: "1h" });
const validResult = TokenManager.validateToken(validToken, testIp);
expect(validResult.valid).toBe(false);
expect(validResult.error).toBe("Too many failed attempts. Please try again later.");
// Make a single failed attempt for each IP
TokenManager.validateToken(invalidToken, ip1);
TokenManager.validateToken(invalidToken, ip2);
const attempts = (TokenManager as any).failedAttempts;
expect(attempts.has(ip1)).toBe(true);
expect(attempts.has(ip2)).toBe(true);
expect(attempts.get(ip1).count).toBe(1);
expect(attempts.get(ip2).count).toBe(1);
expect(attempts.get(ip1).lastAttempt).toBeGreaterThan(0);
expect(attempts.get(ip2).lastAttempt).toBeGreaterThan(0);
});
it("should handle rate limiting for failed attempts", async () => {
const invalidToken = "x".repeat(64);
const uniqueIp = "10.0.0.1";
// Make multiple failed attempts
for (let i = 0; i < 5; i++) {
const result = TokenManager.validateToken(invalidToken, uniqueIp);
expect(result.valid).toBe(false);
if (i < 4) {
expect(result.error).toBe("Invalid token signature");
} else {
expect(result.error).toBe("Too many failed attempts. Please try again later.");
}
}
// Next attempt should be rate limited
const result = TokenManager.validateToken(invalidToken, uniqueIp);
expect(result.valid).toBe(false);
expect(result.error).toBe("Too many failed attempts. Please try again later.");
// Wait for rate limit window to expire
await new Promise(resolve => setTimeout(resolve, MOCK_RATE_LIMIT_WINDOW + 50));
// After window expires, should get normal error again
const finalResult = TokenManager.validateToken(invalidToken, uniqueIp);
expect(finalResult.valid).toBe(false);
expect(finalResult.error).toBe("Invalid token signature");
});
it("should reset rate limits after window expires", async () => {
const invalidToken = "x".repeat(64);
const uniqueIp = "172.16.0.1";
// Make some failed attempts
for (let i = 0; i < 3; i++) {
const result = TokenManager.validateToken(invalidToken, uniqueIp);
expect(result.valid).toBe(false);
expect(result.error).toBe("Invalid token signature");
}
// Wait for rate limit window to expire
await new Promise(resolve => setTimeout(resolve, MOCK_RATE_LIMIT_WINDOW + 50));
// After window expires, should get normal error
const result = TokenManager.validateToken(invalidToken, uniqueIp);
expect(result.valid).toBe(false);
expect(result.error).toBe("Invalid token signature");
// Should have one new attempt recorded
const attempts = (TokenManager as any).failedAttempts;
expect(attempts.has(uniqueIp)).toBe(true);
expect(attempts.get(uniqueIp).count).toBe(1);
});
});
});