4.8 KiB
4.8 KiB
QA procedure: trusted_proxy_client_ip_for_rate_limiting
Package: test (black-box), extending test/login_and_password_reset_rate_limiting_http_test.go's
setup (mockUserRepo, stubHasher, mockMailRepo, the full useCases set, auth.NewHandler(...)).
Test infrastructure additions needed first
- Implement
api/middleware.TrustedProxyRealIP(trustedProxies []string) func(http.Handler) http.Handler. - In tests, build a
chi.NewRouter()mirroringNewAuthRouter's/loginand/password-resetgroups:middleware.TrustedProxyRealIP(trustedProxies)thenmiddleware.NewRateLimiter(maxAttempts, window)then the handler.chimiddleware.RealIPis not part of this router. - Simulate a request's connection address by setting
req.RemoteAddr = "<ip>:<port>"(host:port, matching real request shape) before calling the router. Simulate a proxy-forwarded header withreq.Header.Set("X-Real-IP", "<ip>")orreq.Header.Set("X-Forwarded-For", "<ip>")as needed.
Setup shared by every case
- Call
validator.InitValidator()once. - Build
mockUserRepo,mockMailRepo,stubHasher, and the fulluseCasesset, thenauth.NewHandler(...). - Use
AUTH_RATE_LIMIT_MAX_ATTEMPTS=1and a window covering the whole test (window=time.Hour) throughout, so a second request from the same effective key is always the one that trips 429 and no scenario needs to wait out a window. - Seed
mockUserRepowith user "alice", a correct password hash, soPOST /v1/auth/logincan return 200 where a scenario needs a non-429, non-403 baseline; a wrong-password body works equally well to prove "not rejected by rate limiting" since only the status code being not 429 matters for these cases (assert!= 429in addition to whatever status login returns for the given credentials).
HTTP-level checks
trustedProxies = nil(empty). SendPOST /v1/auth/loginwithRemoteAddr = "203.0.113.5:1"and headerX-Real-IP: 198.51.100.1. Assert the response is not429. Send a second, identical request (sameRemoteAddr, same header). Assert it is429— proves the header had no effect; both requests hit the connection-address counter.trustedProxies = ["10.0.0.1"]. SendPOST /v1/auth/loginwithRemoteAddr = "203.0.113.5:1"(not in the trusted list) and headerX-Real-IP: 1.1.1.1. Assert not429. Send a second request from the sameRemoteAddrwith headerX-Real-IP: 2.2.2.2(a different value). Assert it is429— proves an untrusted source can't split its own attempts across counters by varying the header.trustedProxies = ["10.0.0.1"]. SendPOST /v1/auth/loginwithRemoteAddr = "10.0.0.1:1"and headerX-Real-IP: 198.51.100.9. Assert not429. Send a second request with the sameRemoteAddrand the same header. Assert429— proves the trusted proxy's forwarded client is honored as the counter key.trustedProxies = ["10.0.0.1"]. SendPOST /v1/auth/loginwithRemoteAddr = "10.0.0.1:1"and headerX-Real-IP: 198.51.100.9, consuming that forwarded client's limit (assert not429). Then send a second request with the sameRemoteAddrbut headerX-Real-IP: 198.51.100.10. Assert not429— a different forwarded client behind the same trusted proxy has its own counter.trustedProxies = ["10.0.0.1"]. SendPOST /v1/auth/loginwithRemoteAddr = "10.0.0.1:1"and headerX-Forwarded-For: 198.51.100.9(noX-Real-IP). Assert not429. Send a second, identical request. Assert429— provesX-Forwarded-Foris never read, even from a trusted proxy; both requests are keyed on the proxy's own address10.0.0.1.trustedProxies = ["10.0.0.1"]. SendPOST /v1/auth/loginwithRemoteAddr = "10.0.0.1:1"and no forwarding header at all. Assert not429. Send a second, identical request. Assert429— a trusted proxy with nothing to forward falls back to its own address as the key.
Manual verification (not exercised by the Go test)
- Real behaviour behind the documented nginx and Caddy reverse proxies (
docs/deployment.md) — that each setsX-Real-IPfrom the actual peer address rather than a client-controlled value, and thatTRUSTED_PROXIESon the RenewCMS host is set to the proxy's real address (e.g.127.0.0.1when co-located) — is not exercised by an in-processhttptestrouter and should be checked by hand against a real reverse-proxy deployment. config.NewSettingsFromEnvparsingTRUSTED_PROXIESfrom the environment (semicolon-separated, matchingCORS_ALLOWED_ORIGINS) is a use-case-free config concern; if a pure Go test forinternal/infrastructure/configalready exists forCORS_ALLOWED_ORIGINS(test/runtime_settings_from_env_test.go), extend it with a parallel case rather than adding it here — flagged for the coder/qa stage to place correctly.