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
Preauth
For when you want to expose a web service without letting the whole world try to access it. Because sometimes you want both a belt and suspenders.
I found myself needing to make my personal Nextcloud instance available outside my VPN, but was worried since it has had authentication exploits in the past.
So, I built a simple authentication gateway, which eventually turned into this project.
It sits between your reverse proxy and web service to add extra protection, while still being easy to access from anywhere.
Development
Code Style
This project follows PSR-12 and includes php-cs-fixer as a dev dependency.
# Check for style violations
vendor/bin/php-cs-fixer fix --dry-run --diff
# Auto-fix
vendor/bin/php-cs-fixer fix
Running Tests
vendor/bin/phpunit
Requirements
- Docker
- Caddy (as a reverse proxy)
- a web service you want to secure
It may be possible to use some other reverse proxy, but for now, I'm going to stick with just Caddy.
There is an example Caddyfile in /docs/ and an example.env file to get you started. Within the Caddyfile is a snippet, which makes it easy to wrap your web service with preauth.
When someone tries to reach your protected web service, Caddy will check with preauth if they are allowed, if their preauth cookie is missing, invalid, or expired, we will show them to a login screen.
I say login, but it's really just a TOTP code (6-digit code which changes every 30 second). But once they enter the right code,they'll get their cookie and be shown the protected service. It is also possible to allow all requests from an approved IP address, but that is disabled by default.
First time you spin up the docker container it will generate a TOTP secret (which you'll load into your authenticator app); or generate you own.
Be sure to save that TOTP secret to your docker environment, so that it persists beyond removing the container.
Backup Codes
It is possible to generate single-use backup codes via a console command within the docker container.
docker exec -t preauth bin/console app:generate-backup-codes [count=10]
History
v0.7.0 (May 29th, 2026)
Added ability to generate single-use backup codes. Removed static password and lookup token, as they were security risks. Updated to PHP 8.5, updated dependencies.
v0.6.0 (Feb 10th, 2026)
Added optional (disabled by default) ability to lookup token by static password.
v0.5.0 (Jan 17th, 2026)
Nonce related cleanup; added optional (disabled by default) ability to use a static password as a backup means of authentication.
v0.4.1 (Dec 26th, 2025)
Fixed bug which can occur if you delete cache files.
v0.4.0 (Dec 26th, 2025)
Massive rewrite to switch to using listeners instead of controller, header for login payload instead of get request, removed icon system, asset system, was able to remove all the domain processing, enhanced cookie security, and more.
v0.3.0 (Dec 15th, 2025)
Includes significant breaking changes. Default port and transportation changed to http via port 80. Names of environment variables have changed.
v0.2.0 (Dec 3rd, 2025)
Now with login rate limiting. New page for client error (too many requests). Made example docker compose.
v0.1.0 (Nov 14th, 2025)
Now an actual project, docker image pushed to docker hub, which uses php-fpm, code into a src folder, templates into separate files.
v0.0.1 (June 26th, 2024)
Started off as a single file script which was part of my caddy config. Hardcoded TOTP secret, zero flexibility, but functional. Would stay like that, quietly working in production for about a full year before any real change.