diff --git a/.gitea/workflows/develop.yaml b/.gitea/workflows/develop.yaml index f0df39c..7e97525 100644 --- a/.gitea/workflows/develop.yaml +++ b/.gitea/workflows/develop.yaml @@ -31,4 +31,3 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | ${{ vars.DOCKERHUB_TARGET }}:develop - diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml index 5a227ca..ea293f3 100644 --- a/.gitea/workflows/docker.yaml +++ b/.gitea/workflows/docker.yaml @@ -3,7 +3,7 @@ name: Push Docker on: push: tags: - - '*.*.*' + - 'v*.*.*' jobs: docker: @@ -22,6 +22,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract version + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Build image uses: docker/build-push-action@v5 with: @@ -30,5 +34,4 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | ${{ vars.DOCKERHUB_TARGET }}:latest - ${{ vars.DOCKERHUB_TARGET }}:${{ github.ref_name }} - + ${{ vars.DOCKERHUB_TARGET }}:${{ steps.version.outputs.VERSION }} diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index eba581a..d433b74 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -25,7 +25,7 @@ jobs: SYNC_TOKEN: ${{ secrets.SYNC_GITHUB_TOKEN }} SYNC_TARGET: ${{ vars.SYNC_GITHUB_TARGET }} run: | - git remote add github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET" + git remote add github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET" 2>/dev/null || git remote set-url github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET" - name: Push Current Branch run: | diff --git a/.gitignore b/.gitignore index afa4fc1..de08fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ /.php-cs-fixer.php /.php-cs-fixer.cache ###< friendsofphp/php-cs-fixer ### +.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f3c8af5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,146 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3100f7a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing to Preauth + +Thank you for your interest in contributing to Preauth! This document +outlines the process for contributing to the project. + +## Development Setup + +1. Clone the repository +2. Install dependencies: `composer install` +3. Copy `.env.example` to `.env` and configure as needed +4. Run tests: `vendor/bin/phpunit` + +## Code Style + +This project follows [PSR-12](https://www.php-fig.org/psr/psr-12/) and +includes `php-cs-fixer` as a dev dependency. + +```bash +# Check for style violations +vendor/bin/php-cs-fixer fix --dry-run --diff + +# Auto-fix +vendor/bin/php-cs-fixer fix +``` + +All code must pass the style check before it can be merged. + +## Testing + +All code changes must include tests. The project maintains 100% code +coverage — new code must be fully tested. + +```bash +# Run tests +vendor/bin/phpunit + +# Run with coverage (requires Xdebug) +XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text +``` + +### Test Structure + +- **Unit tests** go in `tests/Unit/` and mirror the `src/` directory structure +- **Functional tests** go in `tests/Functional/` and test the full HTTP kernel +- Use the support traits (`TotpTestHelper`, `ListenerTestHelper`) for + reusable test fixtures + +## Pull Request Process + +1. Create a feature branch from `main` +2. Make your changes, ensuring tests pass and code style is clean +3. Update documentation if needed (README, CHANGELOG, docs/) +4. Submit a pull request to `main` + +### Commit Messages + +Use conventional commit format: + +- `feat:` new feature +- `fix:` bug fix +- `docs:` documentation only +- `refactor:` code change that neither fixes a bug nor adds a feature +- `test:` adding or correcting tests +- `chore:` build process, tooling, etc. + +## Architecture + +Preauth is an event-listener-driven Symfony application (no controllers). +See `ROADMAP.md` for the full architecture overview and design decisions. + +## License + +By contributing, you agree that your contributions will be licensed under +the MIT License. diff --git a/DESIGN_CONSIDERATIONS.md b/DESIGN_CONSIDERATIONS.md new file mode 100644 index 0000000..32203db --- /dev/null +++ b/DESIGN_CONSIDERATIONS.md @@ -0,0 +1,298 @@ +# Design Considerations — Preauth + +## Summary + +Preauth is a well-architected TOTP-based authentication gateway that has evolved from a single-file script into a clean, event-listener-driven Symfony application with 100% test coverage. The codebase demonstrates strong security fundamentals (host-prefixed cookies, nonce-based replay protection, rate limiting, backup code system) and thoughtful operational design (dual-layer cache with change tracking, FrankenPHP worker mode). + +This document was originally prepared as a design review. Items that have been addressed are marked with ✅ and include a reference to the commit or change that resolved them. Items still open are marked with ⬜ and remain as recommendations for future work. + +--- + +## 1. Security + +### 1.1 Missing Security Response Headers [HIGH PRIORITY] ✅ Addressed + +**Current state:** Fixed. A `SecurityHeadersListener` (response event, priority 0) now sets the following headers on all main-request responses: + +``` +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +``` + +The inline `