120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
import { validateCsrfOrigin } from '../src/lib/auth/csrf-guard';
|
|
import { GET as vkStartGet } from '../src/app/api/auth/vk/start/route';
|
|
import { oauthStartRateLimiter } from '../src/lib/rate-limiter';
|
|
|
|
describe('Phase 2.2.3 Origin, CSRF Trusted Host & Rate Limiting Gate', () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe('Production Base URL & VK_REDIRECT_URI Fail-Fast Policy', () => {
|
|
it('fails fast in production when APP_BASE_URL is missing', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
delete process.env.APP_BASE_URL;
|
|
|
|
expect(() => getAppBaseUrl()).toThrow(/APP_BASE_URL environment variable is strictly required in production/i);
|
|
});
|
|
|
|
it('fails fast in production when APP_BASE_URL is not HTTPS', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.APP_BASE_URL = 'http://insecure-http-url.com';
|
|
|
|
expect(() => getAppBaseUrl()).toThrow(/must be a valid HTTPS URL in production/i);
|
|
});
|
|
|
|
it('fails fast in production when VK_REDIRECT_URI is missing', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.APP_BASE_URL = 'https://randomayzer.org';
|
|
delete process.env.VK_REDIRECT_URI;
|
|
|
|
expect(() => getVkRedirectUri()).toThrow(/VK_REDIRECT_URI environment variable is strictly required in production/i);
|
|
});
|
|
|
|
it('accepts valid HTTPS configuration in production', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.APP_BASE_URL = 'https://randomayzer.org';
|
|
process.env.VK_REDIRECT_URI = 'https://randomayzer.org/api/auth/vk/callback';
|
|
|
|
expect(getAppBaseUrl()).toBe('https://randomayzer.org');
|
|
expect(getVkRedirectUri()).toBe('https://randomayzer.org/api/auth/vk/callback');
|
|
});
|
|
});
|
|
|
|
describe('CSRF Trusted Host & Host-Spoofing Immunity', () => {
|
|
it('rejects attacker sending evil Origin even if attacker injects spoofed X-Forwarded-Host', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.APP_BASE_URL = 'https://trusted-randomayzer.org';
|
|
|
|
const req = new NextRequest('http://localhost/api/auth/logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
origin: 'https://evil.com',
|
|
'x-forwarded-host': 'evil.com', // Spoofed header
|
|
},
|
|
});
|
|
|
|
expect(() => validateCsrfOrigin(req)).toThrow(/CSRF origin mismatch/i);
|
|
});
|
|
|
|
it('rejects cross-site Sec-Fetch-Site requests', () => {
|
|
const req = new NextRequest('http://localhost/api/auth/logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
origin: 'https://trusted-randomayzer.org',
|
|
'sec-fetch-site': 'cross-site',
|
|
},
|
|
});
|
|
|
|
expect(() => validateCsrfOrigin(req)).toThrow(/cross-site origin rejected/i);
|
|
});
|
|
|
|
it('accepts valid origin matching configured trusted host', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.APP_BASE_URL = 'https://trusted-randomayzer.org';
|
|
|
|
const req = new NextRequest('https://trusted-randomayzer.org/api/auth/logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
origin: 'https://trusted-randomayzer.org',
|
|
'sec-fetch-site': 'same-origin',
|
|
},
|
|
});
|
|
|
|
expect(() => validateCsrfOrigin(req)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('OAuth Start Rate Limiter', () => {
|
|
it('rate limits burst requests to GET /api/auth/vk/start', async () => {
|
|
oauthStartRateLimiter.clear();
|
|
|
|
const ip = '198.51.100.42';
|
|
|
|
// 10 allowed requests
|
|
for (let i = 0; i < 10; i++) {
|
|
const req = new NextRequest('http://localhost:3000/api/auth/vk/start', {
|
|
headers: { 'x-forwarded-for': ip },
|
|
});
|
|
const res = await vkStartGet(req);
|
|
expect(res.status).toBe(307); // Temporary redirect to VK
|
|
}
|
|
|
|
// 11th request in the same window -> 429 Too Many Requests
|
|
const blockedReq = new NextRequest('http://localhost:3000/api/auth/vk/start', {
|
|
headers: { 'x-forwarded-for': ip },
|
|
});
|
|
const blockedRes = await vkStartGet(blockedReq);
|
|
expect(blockedRes.status).toBe(429);
|
|
const body = await blockedRes.json();
|
|
expect(body.error?.message).toMatch(/rate limit exceeded/i);
|
|
});
|
|
});
|
|
});
|