diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index c61460b3..30fd8a41 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -1131,8 +1131,26 @@ func (h *AuthHandler) consumeOAuthState(ctx context.Context, state string) bool // remember to escape — defense in depth. func renderAuthError(c *fiber.Ctx, status int, headline, detail string) error { c.Set("Content-Type", "text/html; charset=utf-8") + // BUG-API-404 (QA 2026-05-29): the OAuth / magic-link callback HTML + // is a per-request rendering of session-bound state — a back-button, + // browser-history-restore, or service-worker re-fetch must NOT replay + // it (the underlying token has been consumed or expired). Without + // Cache-Control, the body could be re-served by the browser fetch + // cache or any intermediary, which both leaks the "you tried this + // link" UX state across sessions AND, in the success-redirect cousin + // of this surface, would re-set the exchange cookie. `no-store` + // (RFC 9111 §5.2.2.5) is the strongest stop-cache directive and + // matches the contract every other auth-result surface in the api + // already follows. + c.Set(fiber.HeaderCacheControl, "no-store") + // BUG-API-257 (QA 2026-05-29): the element used to ship with + // no `lang` attribute. WCAG 3.1.1 "Language of Page" requires a + // programmatically determinable primary language; assistive tech + // (VoiceOver, NVDA) falls back to the OS locale otherwise, mis- + // pronouncing English copy in non-English locales. `lang="en"` is + // the correct value for the static English-only copy below. body := fmt.Sprintf(` - + Sign-in error

%s

diff --git a/internal/handlers/auth_helpers_coverage_test.go b/internal/handlers/auth_helpers_coverage_test.go index b901495a..7603b709 100644 --- a/internal/handlers/auth_helpers_coverage_test.go +++ b/internal/handlers/auth_helpers_coverage_test.go @@ -263,12 +263,24 @@ func TestAuth_RenderAuthError_StatusAndContentType(t *testing.T) { defer resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Contains(t, resp.Header.Get("Content-Type"), "text/html") + // BUG-API-404 (QA 2026-05-29): the OAuth / magic-link callback HTML + // is per-request session-bound state — back-button or service-worker + // re-fetch must NOT replay it. Pin Cache-Control: no-store so a + // future regression that drops the c.Set call fails before merge. + assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"), + "BUG-API-404: renderAuthError must stamp Cache-Control: no-store on every callback HTML response") buf := make([]byte, 1024) n, _ := resp.Body.Read(buf) body := string(buf[:n]) assert.Contains(t, body, "Sign-in error") assert.Contains(t, body, "Hello") assert.Contains(t, body, "Detail") + // BUG-API-257 (QA 2026-05-29): WCAG 3.1.1 — the <html> element MUST + // carry a lang attribute so assistive tech (VoiceOver, NVDA) doesn't + // fall back to the OS locale and mispronounce English copy. Pin the + // value here so a future revert to the bare <html> fails before merge. + assert.Contains(t, body, `<html lang="en">`, + "BUG-API-257: renderAuthError HTML must include lang=\"en\" on the <html> element (WCAG 3.1.1)") } // SEC-API FINDING-23 regression: renderAuthError must HTML-escape both