The test workflow was hitting GitHub's unauthenticated API rate
limit (60 req/hour) when downloading 95 packages via composer
install --prefer-dist, causing 429 Too Many Requests errors.
Two fixes applied:
1. Cache Composer's download cache (~/.composer/cache) keyed on
composer.lock hash, so repeated CI runs don't re-download
packages at all.
2. Configure GitHub OAuth token via SYNC_GITHUB_TOKEN secret to
raise the rate limit to 5,000 req/hour for cache misses.
When subdomain redirection is off, the login form is served inline on
the protected host and submission happens via a same-origin fetch() call
in _script.html.twig. The CSP default-src 'none' was blocking that
fetch (connect-src falls back to default-src).
Add connect-src 'self' to the CSP only when the request is not on the
auth subdomain (i.e. when the inline script is present). On the auth
subdomain the form POSTs normally with no inline script, so the stricter
policy still applies there.
This is the least-privilege relaxation: only same-origin connections,
only on pages that need them.
The host-prefix regex required at least one character after the slash
(/\+.+/), so a pattern like 'code.example.com/' was silently dropped
instead of matching the root path '/'. Changed \+.+ to \+.* so the
trailing slash alone is accepted as the path '/'.
Added tests covering the exact bug scenario from PUBLIC_PATHS
config: 'code.digitaladapt.com/,code.digitaladapt.com/public/**'
The Twig |e('css') filter was escaping '#' (0x23) to '\23 ' in hex
color values (bg_color, fg_color, error_color), causing browsers to
not recognize them as valid CSS colors. These are admin-configured
environment variables, not user input, so CSS escaping is unnecessary.
Manages a local PHP dev server for end-to-end development and testing.
Binds to 0.0.0.0:8773, accessible via Caddy at
https://preauth.lyra-dev.devgnome.com.
Features:
- Self-bootstrapping: installs PHP 8.4 + extensions (including APCu,
which is critical for nonce cache, rate limiter, and session storage),
Composer, and project dependencies if missing. Survives terminal
resets/reboots.
- Enables apc.enable_cli=1 for console commands (matches Dockerfile)
- Subcommands: start, stop, status, restart
- Sets APP_SHARE_DIR to var/share for filesystem session persistence
- Clears dev cache on start
No database needed — preauth uses APCu + filesystem cache exclusively.
Port assignment: P-R-E = 7-7-3 → 8773
Re-adds 'develop' to push triggers in develop.yaml and tests.yaml
so the :develop Docker image can be built from the develop branch,
enabling dev testing without requiring a merge to main.
Add PublicAccessListener (priority 84) that allows rate-limited
unauthenticated access to configured public paths. Authenticated users
bypass this listener entirely via AcceptListener/AllowListener.
New components:
- PublicPathMatcher service with wildcard path matching (* and **)
and optional host-prefix scoping
- PublicAccessListener applying per-IP rate limiting to public paths
- Separate public_limiter compound rate limiter (burst + sustained)
- publicRateLimitCache pool (APCu in prod, array in tests)
New env vars:
- PUBLIC_PATHS (comma-separated path patterns, empty = disabled)
- PUBLIC_BURST_COUNT/PUBLIC_BURST_TIME (default 100/60s)
- PUBLIC_UPPER_COUNT/PUBLIC_UPPER_TIME (default 500/3600s)
Tests: 52 new tests (29 unit for PublicPathMatcher, 12 unit for
PublicAccessListener, 11 functional for PublicAccessFlowTest).
Total: 293 tests, 605 assertions, all passing.
PHP CS Fixer: 0 of 63 files need fixing.
Documentation: README, CHANGELOG, ROADMAP, Caddyfile, example.env
all updated with public access configuration and examples.
Add REMOTE_USER env var with four modes:
- session (default): sends session id, backward-compatible
- static: sends a fixed string (REMOTE_USER_STATIC)
- mapped: looks up session id in REMOTE_USER_MAP
- none: omits the header entirely
New RemoteUserMode enum, ConfigBag parsing/validation, and
StringTrait::authSuccessResponse resolves the header value based
on the configured mode. AcceptListener now receives ConfigBag as
a constructor dependency.
Addresses design consideration 1.2 (Remote-User header value is
user-controlled) from DESIGN_CONSIDERATIONS.md.
241 tests pass, 0 cs-fixer violations.
- Mark all resolved items with ✅ and describe the fix applied
- Mark remaining open items with ⬜ and keep recommendations
- Add new sections for items discovered during the fix work:
- 1.7 CSS injection in style template
- 1.8 ->json null safety
- 1.9 validReturn() parse_url false check
- 1.10 Incomplete TLD list
- 2.8 Duplicated response construction
- 2.9 Duplicated constants
- 5.3 Kernel::terminate() try/finally
- 9. CI & Workflows (tag format, stale branches, publish.yaml)
- Update 'What's Done Well' to reflect new improvements
- Add summary noting this is a living document tracking the
fix/v1.0-must-fix branch state
Documentation:
- Create CHANGELOG.md with full version history (v0.0.1 through unreleased)
- Rewrite README with comprehensive setup guide, configuration reference,
architecture overview, security model, and feature list
- Update ROADMAP.md: fix branch status table, mark completed security
review items, update TOTP leeway description
- Fix 'centeral' typo in docs/Caddyfile
- Remove TODO comment from docs/compose.yaml
- Add DESIGN_CONSIDERATIONS.md (design review document)
Code quality:
- Extract duplicated cookie name/domain logic into CookieNameTrait
methods: sessionCookieName() and sessionCookieDomain()
- Update AcceptListener, AllowListener, InterceptListener, and
LoginManager to use the shared methods
- Remove fragile cross-file coupling comment between LoginManager
and InterceptListener
Error handling:
- Wrap cache operations in AcceptListener and AllowListener with
try/catch to fail closed (don't authenticate on cache errors)
- Log cache errors at error level instead of propagating as 500s
- Early return pattern in AcceptListener and AllowListener for
cleaner control flow
Security:
- Add SecurityHeadersListener (X-Content-Type-Options, X-Frame-Options,
CSP, Referrer-Policy, HSTS)
- Replace document.write() with document.documentElement.innerHTML
in login JS to avoid CSP violations
- Add CSS escaping (|e('css')) to env color values in _style.html.twig
- Document CSRF protection model: nonce serves as CSRF token for POST
form path (single-use, server-generated, 120s TTL)
- Reduce TOTP verification window from 10 periods (±5 min) to 1 (±30s)
- Remove hardcoded APP_SECRET from bin/franken.sh (now uses env or
generates random)
- Remove backup code values from debug log output
- Add .env to .gitignore
Bug fixes:
- Fix ->json access on possibly-null in LoginListener
(uses null-safe operator ?->)
- Fix validReturn() not checking false from parse_url (could cause
TypeError on malformed URLs)
- Add isHit() race condition check in AcceptListener and AllowListener
- Add try/finally in Kernel::terminate() so parent::terminate() always
runs even if persist() throws
- Add input validation to GenerateBackupCodesCommand (reject count < 1)
- Use Response::HTTP_INTERNAL_SERVER_ERROR constant in GetTotpTrait
instead of literal 500
Docker/CI:
- Explicitly install curl in Docker final image (needed for healthcheck)
- Update workflow tag pattern to v*.*.* (standardize on v-prefix)
- Extract version without v-prefix for Docker image tag
- Remove stale develop branch from CI triggers
- Fix publish.yaml git remote add to use set-url on re-runs
Code quality:
- Add declare(strict_types=1) to all interface files
- Add #[AsCommand] attribute to GenerateBackupCodesCommand
- Fix BackupCodeInterface default count to match implementation (10)
- Lowercase host before TLD lookup in DomainManager
- Expand TLD list with many missing multi-part TLDs (.com.au, .co.jp,
.com.br, .co.kr, .com.tw, .co.za, etc.) to prevent open redirect
vulnerabilities
- Disable unused Symfony sessions in framework.yaml
Tests:
- Update DomainManagerTest for corrected TLD parsing (.com.au, .co.jp,
.com.br now correctly recognized as multi-part)
- Update GetTotpTraitTest for corrected error message
- Update GenerateBackupCodesCommandTest: zero count now throws exception
- Add .dockerignore to exclude .git, vendor, var, tests, docs, .env
and other non-build files from Docker context
- Fix broken base64url padding in src/Data/Payload.php: str_pad was
a no-op because the length argument was always < string length.
Replaced with correct str_repeat approach
- Fix typo in bin/franken.sh: digtialadapt → digitaladapt
- Add comment to bin/franken.sh noting it's a dev utility
- Remove config/reference.php from git tracking (auto-generated file)
and add to .gitignore
- Fix readme.md: env.example → example.env (matches actual filename)
- Add TestKernel that removes the kernel.reset tag from nonceCache,
rateLimitCache, sessionCache and sessionStorage pools so in-memory
state survives across requests within a single test (mirroring APCu
persistence in production)
- Add config/packages/test/ with array cache adapters and test session
config
- Set fixed TOTP secret (JBSWY3DPEHPK3PXP) and high rate limits in
phpunit.dist.xml and .env.test so functional tests can compute valid
codes and are not rate-limited
- Make Kernel non-final so TestKernel can extend it
- Fix testFailedLoginWithSpentNonceIsRejected and
testConsumedBackupCodeCannotBeReused: clear the CookieJar between
sub-requests so a session cookie set by a prior successful login does
not auto-authenticate the next request via AcceptListener before the
nonce/backup-code path is exercised
- Add phpunit/phpunit ^13.2, symfony/browser-kit and symfony/css-selector
to require-dev, plus the autoload-dev mapping for App\Tests- Add phpunit.dist.xml (strict deprecation/notice/warning failures,
APP_ENV=test forced) and .env.test / bin/phpunit / tests/bootstrap.php
from the PHPUnit recipe
- Add tests/Support/TotpTestHelper providing a deterministic TOTP
fixture, frozen clock and ConfigBag/cache-pool helpers
- Add 121 unit tests covering Clock, ConfigBag, Data/Payload, Enum/Scope,
MonitorCacheKeys, PersistCache, Utilities, all five Traits and the
three Service managers (BackupCode, Domain, Login)
- Fix LoginManagerTest nonce lookups to use makeCacheKey() so the cache
key matches the one the manager actually reads/writes
- Gitignore bin/.phpunit.result.cache
Intending to build support for single-use backup codes.
Started refactoring to move trait dependencies internally, so that classes only have to specify their own direct dependencies.