/** * KTO-01 Kriterium 7 (Drosselung), AUTH-CFG-10 und AUTH-CFG-13. * * Laeuft gegen den ZUSAETZLICHEN GoTrue-Container mit GOTRUE_RATE_LIMIT_HEADER=X-Forwarded-For * (e2e/support/start-throttle-gotrue.sh, THROTTLE_GOTRUE_URL), nicht gegen die CLI: Die CLI * drosselt nie (Spike A2, F11). Jeder Test nutzt eine eigene zufaellige IP (simuliert den Proxy, * R13). Es wird direkt auf GoTrue zugegriffen, ohne Kong und ohne apikey. */ import { expect, test } from '@playwright/test'; import { authConfig, env, errorCodes } from '../../support/auth-config'; import { GoTrueClient, randomEmail, randomIp, randomPassword } from '../../support/gotrue'; const base = env.throttleGotrueUrl; test.skip(!base, 'THROTTLE_GOTRUE_URL nicht gesetzt (siehe e2e/support/start-throttle-gotrue.sh)'); /** Zusaetzliche Versuche, die der Nachfuellrate (0,5/s) der gemessenen Dauer entsprechen: ceil(t * 0,5) + 1. */ const refillTolerance = (elapsedMs: number) => Math.ceil((elapsedMs / 1000) * 0.5) + 1; const client = (ip?: string) => new GoTrueClient({ baseUrl: base!, apikey: null, headers: ip ? { 'X-Forwarded-For': ip } : {} }); /** Konto, das GoTrue-Instanz und DB mit der CLI teilen; Anlage ohne Header ueber die CLI (Service-Role). */ async function createUser() { const email = randomEmail(); const password = randomPassword(); const res = await new GoTrueClient().createConfirmedUser(email, password); expect(res.status).toBe(200); return { email, password }; } test.describe('KTO-01 Kriterium 7: Drosselung der Anmeldung (throttle)', () => { test('KTO-01 Kriterium 7: AUTH-CFG-10 30 Fehllogins einer IP -> invalid_credentials, Login #31 -> 429 over_request_rate_limit', async () => { const { email, password } = await createUser(); const ip = randomIp(); const gotrue = client(ip); const started = Date.now(); for (let attempt = 1; attempt <= authConfig.loginBurst; attempt++) { const res = await gotrue.signIn(email, `${password}-falsch`); expect(res.status, `Versuch ${attempt}`).toBe(400); expect(res.body.error_code, `Versuch ${attempt}`).toBe(errorCodes.invalidCredentials); } const tolerance = refillTolerance(Date.now() - started); // Login #31 -> 429. Der Limiter fuellt 0,5 Token/s nach (AUTH-CFG-10); je Sekunde Laufzeit // der ersten 30 Versuche darf die erste 429 einen halben Versuch spaeter kommen. let limited = await gotrue.signIn(email, `${password}-falsch`); for (let extra = 0; extra < tolerance && limited.status !== 429; extra++) { limited = await gotrue.signIn(email, `${password}-falsch`); } expect(limited.status).toBe(429); expect(limited.body.error_code).toBe(errorCodes.overRequestRateLimit); // Auch der richtige Login dieser IP wird gedrosselt (Drosselung je IP, nicht je Passwort). expect((await gotrue.signIn(email, password)).status).toBe(429); }); test('KTO-01 Kriterium 7: AUTH-CFG-10 die 429-Antwort traegt weder Retry-After noch X-RateLimit-* (App nennt keine Dauer)', async () => { const { email, password } = await createUser(); const gotrue = client(randomIp()); const started = Date.now(); let limited; let attempts = 0; // Hoechstens Burst + Toleranz aus der gemessenen Dauer; danach muss eine 429 gekommen sein. while (attempts < authConfig.loginBurst + refillTolerance(Date.now() - started) + 1) { attempts++; const res = await gotrue.signIn(email, `${password}-falsch`); if (res.status === 429) { limited = res; break; } } if (!limited) throw new Error(`Keine 429 nach ${attempts} Versuchen in ${Date.now() - started} ms`); expect(limited.headers.get('retry-after')).toBeNull(); const rateHeaders = [...limited.headers.keys()].filter((h) => h.toLowerCase().startsWith('x-ratelimit')); expect(rateHeaders).toEqual([]); }); test('KTO-01 Kriterium 7: AUTH-CFG-10 eine zweite IP ist von der Drosselung der ersten nicht betroffen', async () => { const { email, password } = await createUser(); const first = client(randomIp()); for (let attempt = 1; attempt <= authConfig.loginBurst; attempt++) { await first.signIn(email, `${password}-falsch`); } expect((await first.signIn(email, `${password}-falsch`)).status).toBe(429); const second = client(randomIp()); const wrong = await second.signIn(email, `${password}-falsch`); expect(wrong.status).toBe(400); expect(wrong.body.error_code).toBe(errorCodes.invalidCredentials); const right = await second.signIn(email, password); expect(right.status).toBe(200); expect(right.body.access_token).toBeTruthy(); }); test('KTO-01 Kriterium 7: AUTH-CFG-13 Requests ohne X-Forwarded-For werden nicht gedrosselt', async () => { const { email, password } = await createUser(); const gotrue = client(); for (let attempt = 1; attempt <= authConfig.loginBurst + 5; attempt++) { const res = await gotrue.signIn(email, `${password}-falsch`); expect(res.status, `Versuch ${attempt}`).toBe(400); } }); });