Files
preauth/CHANGELOG.md
T
lyra 5258e175a1
Tests / test (pull_request) Successful in 1m8s
fix: never cache the login flow (stale pre-auth responses in Safari)
The login page, failed logins, redirects, and rate-limit/error pages could
be stored by the browser (Symfony's default 'no-cache, private' still
permits storage — it only requires revalidation). Older Safari builds may
then replay a stale pre-auth response on refresh, appearing to log the
user back out, or show a previous session after logging in again.

- SecurityHeadersListener: send strict anti-caching headers on non-2xx
  responses only (no-store/no-cache/must-revalidate/proxy-revalidate,
  max-age=0, s-maxage=0 + Pragma, Expires, Surrogate-Control, Vary: *).
  2xx grants (already-authenticated / public access) are consumed by
  Caddy's forward_auth check and never reach the browser, and protected
  services' own cache headers must stay untouched.
- templates/_script.html.twig: fetch() with cache: 'no-store'; follow
  redirects with location.replace() to keep the login page out of history
  and the back-forward cache.
- docs/Caddyfile: reusable (preauth_no_store) snippet imported into every
  forward_auth block, using header_down so the guarantee holds at the edge
  (verified: replaces conflicting upstream values, leaves service
  responses alone).
- tests: unit coverage for the listener and functional coverage for the
  full HTTP kernel (login/failure/redirect/rate-limit not cacheable;
  200 grants untouched); asserts the rendered page carries the JS changes.
- readme/CHANGELOG updates.
2026-09-14 19:31:44 -04:00

8.4 KiB
Raw Permalink Blame History

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased] — v1.1

Added

  • Public rate-limited access — Select paths can now be made publicly accessible without TOTP authentication, with separate per-IP rate limiting. This is useful for exposing public content (e.g., public Gitea repositories) while protecting server resources from bot traffic.
    • New PUBLIC_PATHS env var: comma-separated path patterns with * (single segment) and ** (cross-segment) wildcard support. Optional host prefix (e.g., code.example.com/public/**). When empty (default), the feature is fully disabled.
    • New PUBLIC_BURST_COUNT / PUBLIC_BURST_TIME env vars for burst rate limiting (default: 100 requests per 60 seconds).
    • New PUBLIC_UPPER_COUNT / PUBLIC_UPPER_TIME env vars for sustained rate limiting (default: 500 requests per 3600 seconds).
    • Authenticated users bypass the public rate limiter entirely.
    • Over-limit responses include a Retry-After header.
    • New PublicPathMatcher service for path pattern matching.
    • New PublicAccessListener (priority 84) in the request pipeline.

Changed

  • Upgraded Symfony 7.4 → 8.1 — All symfony/* components bumped to 8.1.* (resolved to 8.1.2–8.1.6). The 7.4 deprecation sweep was clean (test suite runs with failOnDeprecation), so the major-version jump required no application code changes. See docs/symfony-8.1-upgrade-plan.md.

Removed

  • runtime/frankenphp-symfony — No longer needed: symfony/runtime 8.1 handles FrankenPHP worker mode natively via its built-in FrankenPhpWorkerRunner. The extra.runtime override in composer.json was removed so the runtime auto-detects FrankenPHP. The old package's FRANKENPHP_LOOP_MAX env var is no longer read; an equivalent recycle limit is restored via the new MAX_REQUESTS setting below.

Added

  • MAX_REQUESTS worker-thread recycle limit — The Caddyfile now sets FrankenPHP's native max_requests from the MAX_REQUESTS environment variable: each PHP worker thread is gracefully restarted after N requests while others keep serving, containing slow memory growth across long uptime. The image default is 500 (matching the previous runtime/frankenphp-symfony default), baked in as a Docker build arg and overridable at runtime (MAX_REQUESTS=0 disables restarts). Arbitrary frankenphp-block configuration is still possible via the stock FRANKENPHP_CONFIG env var.

Fixed

  • Login flow responses are no longer cacheable — the login page, failed logins, redirects, and rate-limit/error pages now send strict anti-caching headers (Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0 plus Pragma, Expires, Surrogate-Control, and Vary: *), the login form's fetch() bypasses the HTTP cache, and the example Caddyfile guards every forward_auth block with matching header_down rules. This prevents browsers — notably older Safari — from replaying a stale pre-auth response on refresh (previously: log in successfully, refresh, and land back on the login page). Successful (2xx) responses are deliberately excluded: they are consumed by the proxy's forward_auth check and never reach the browser.

[1.0.0] — v1.0 Release

Security

  • Made Remote-User header value configurable via REMOTE_USER environment variable with four modes: session (default), static, mapped, and none. This allows deployments to prevent user-controlled header values from reaching backend services.
  • Added SecurityHeadersListener to set X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, and Strict-Transport-Security headers on all responses.
  • Replaced document.write() with document.documentElement.innerHTML in login page JavaScript to avoid CSP violations.
  • Added CSS escaping (|e('css')) to environment-configured color values in the login page template to prevent CSS injection.
  • Documented CSRF protection model: the nonce system provides CSRF protection for POST form logins (server-generated, single-use, 120s TTL).
  • Reduced TOTP verification window from 10 periods (±5 minutes) to 1 period (±30 seconds) to reduce brute-force attack surface.
  • Removed hardcoded APP_SECRET from bin/franken.sh (now uses environment variable or generates a random secret).
  • Removed backup code values from debug log output.
  • Added .env to .gitignore.
  • Expanded TLD list in DomainManager with many missing multi-part TLDs (.com.au, .co.jp, .com.br, .co.kr, .com.tw, .co.za, etc.) to prevent open redirect vulnerabilities from incorrect domain matching.
  • Lowercased host before TLD lookup to fix case-sensitivity issue.

Fixed

  • Fixed $payload->json access on possibly-null $payload in LoginListener using null-safe operator (?->).
  • Fixed validReturn() not checking false return from parse_url(), which could cause a TypeError on malformed URLs.
  • Added isHit() race condition check in AcceptListener and AllowListener between hasItem() and getItem() calls.
  • Added try/finally in Kernel::terminate() so parent::terminate() always runs even if persist() throws an exception.
  • Added input validation to GenerateBackupCodesCommand — rejects count < 1.

Changed

  • Disabled unused Symfony sessions in framework.yaml (preauth implements its own cookie/cache-based session management).
  • Standardized git tag format to use v prefix (v1.0.0 instead of 1.0.0).
  • Updated CI workflows to use v*.*.* tag pattern and strip v prefix for Docker image tags.
  • Removed stale develop branch from CI triggers.
  • Fixed publish.yaml to use git remote set-url on re-runs instead of failing when the remote already exists.
  • Explicitly install curl in the Docker final image (needed for healthcheck).
  • Added declare(strict_types=1) to all interface files.
  • Added #[AsCommand] attribute to GenerateBackupCodesCommand.
  • Fixed BackupCodeInterface default count to match implementation (10).
  • Used Response::HTTP_INTERNAL_SERVER_ERROR constant in GetTotpTrait instead of literal 500.

[0.10.0] - 2026-08-11

Added

  • PHP-CS-Fixer with PSR-12 configuration and CI check.

[0.9.0] - 2026-07-15

Added

  • PHPUnit test suite — 222 tests, 100% code coverage (lines, methods, classes).

[0.8.1] - 2026-05-30

Fixed

  • Bug fixes and cleanup from develop branch merge.

[0.8.0] - 2026-05-29

Changed

  • Renamed form fields for clarity.
  • Fixed invalid login bug.

[0.7.0] - 2026-05-29

Added

  • Single-use backup codes via app:generate-backup-codes console command.
  • Cache persistence improvement — only write changed keys to file storage.

Removed

  • Static password and lookup token (security risks).

Changed

  • Updated to PHP 8.5, updated dependencies.

[0.6.0] - 2026-02-10

Added

  • Optional (disabled by default) ability to lookup token by static password.

[0.5.0] - 2026-01-17

Added

  • Optional (disabled by default) ability to use a static password as backup auth.

Changed

  • Nonce-related cleanup.

[0.4.1] - 2025-12-26

Fixed

  • Bug which can occur if cache files are deleted.

[0.4.0] - 2025-12-26

Changed

  • Massive rewrite to listener-based architecture instead of controllers.
  • Login payload sent via X-Preauth header instead of GET request parameters.
  • Enhanced cookie security.
  • Removed icon system and asset system.

[0.3.0] - 2025-12-15

Changed

  • Breaking: Default port and transport changed to HTTP on port 80.
  • Breaking: Environment variable names have changed.
  • Refactored to Symfony 7.4 with FrankenPHP.

[0.2.0] - 2025-12-03

Added

  • Login rate limiting (burst + upper window).
  • Error page for rate-limited clients ("too many requests").
  • Example Docker Compose file.

[0.1.0] - 2025-11-14

Added

  • Docker image published to Docker Hub.
  • PHP-FPM based, code in src/, templates in separate files.

[0.0.1] - 2024-06-26

Notes

  • Started as a single-file script in Caddy config. Hardcoded TOTP secret, zero flexibility, but functional. Ran quietly in production for about a year before any real development began.