All checks were successful
publish-latest / docker (push) Successful in 22s
Tighten forward-auth return URL validation to reject unsafe host syntax, including backslash-based browser/parser mismatches, and normalize scheme/port matching so bare allowlist entries are HTTPS default-port only. Support explicit scheme-qualified targets, including IPv6 host:port entries. Always emit deterministic identity headers with empty values when attributes are absent, zero unsafe header values, and clear stale forward-auth return targets when OIDC or registration flows take precedence. Add regression coverage for wildcard redirect bypasses, scheme/port handling, IPv6 allowlist entries, empty identity headers, unsafe header values, and stale return-target cleanup. Update docs with proxy header-stripping guidance and the new allowlist semantics.
235 lines
7 KiB
Markdown
235 lines
7 KiB
Markdown
# Porchlight
|
|
|
|
OpenID Connect Provider with user management, built with FastAPI.
|
|
|
|
Porchlight handles user registration (via magic links), credential management
|
|
(passwords and WebAuthn security keys), and issues OIDC tokens for relying
|
|
parties. It uses SQLite for storage, Jinja2 + HTMX for the UI, and idpyoidc
|
|
for the OIDC protocol layer.
|
|
|
|
## Production Setup
|
|
|
|
### Docker (recommended)
|
|
|
|
```bash
|
|
docker compose --profile prod up --build
|
|
```
|
|
|
|
This starts Porchlight on port 8000 with 4 uvicorn workers. Data is persisted
|
|
in a named Docker volume.
|
|
|
|
Set required environment variables in `docker-compose.yml` or via `.env`:
|
|
|
|
```bash
|
|
OIDC_OP_ISSUER=https://auth.example.com
|
|
OIDC_OP_SESSION_SECRET=<random-secret>
|
|
```
|
|
|
|
### Manual
|
|
|
|
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
|
|
|
|
```bash
|
|
uv sync --no-dev
|
|
|
|
OIDC_OP_ISSUER=https://auth.example.com \
|
|
OIDC_OP_SESSION_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))") \
|
|
uv run uvicorn porchlight.app:create_app \
|
|
--factory --host 0.0.0.0 --port 8000 --workers 4
|
|
```
|
|
|
|
### Bootstrap the first admin user
|
|
|
|
After starting the app for the first time, create an admin account:
|
|
|
|
```bash
|
|
OIDC_OP_ISSUER=https://auth.example.com \
|
|
uv run porchlight initial-admin admin
|
|
```
|
|
|
|
This prints a one-time registration URL. Open it in a browser to set up
|
|
credentials (password or security key) for the admin user.
|
|
|
|
### CLI commands
|
|
|
|
Porchlight includes a CLI for administrative tasks. All commands read the same
|
|
`OIDC_OP_*` environment variables as the server.
|
|
|
|
**`porchlight create-invite <username>`** -- Generate a magic link registration
|
|
URL for a new user.
|
|
|
|
```bash
|
|
uv run porchlight create-invite alice
|
|
uv run porchlight create-invite alice --ttl 3600 --note "Onboarding"
|
|
```
|
|
|
|
| Option | Description |
|
|
|---|---|
|
|
| `--ttl SECONDS` | Link expiration (default: `OIDC_OP_INVITE_TTL`, 86400s) |
|
|
| `--note TEXT` | Optional note stored with the link |
|
|
|
|
**`porchlight initial-admin <username>`** -- Bootstrap the first admin user with
|
|
a registration link.
|
|
|
|
```bash
|
|
uv run porchlight initial-admin admin
|
|
uv run porchlight initial-admin admin --group admin --group superusers
|
|
```
|
|
|
|
| Option | Description |
|
|
|---|---|
|
|
| `--group TEXT` | Groups to assign (repeatable, default: `admin`, `users`) |
|
|
|
|
### Configuration
|
|
|
|
All settings are read from environment variables with the `OIDC_OP_` prefix.
|
|
Settings can also be provided via a TOML config file (see below). Environment
|
|
variables always take priority over file values.
|
|
|
|
| Variable | Default | Description |
|
|
|---|---|---|
|
|
| `OIDC_OP_ISSUER` | **required** | OIDC issuer URL (must match public URL) |
|
|
| `OIDC_OP_SESSION_SECRET` | random per process | Session cookie signing secret |
|
|
| `OIDC_OP_DEBUG` | `false` | Enable `/docs` Swagger UI |
|
|
| `OIDC_OP_SQLITE_PATH` | `data/oidc_op.db` | SQLite database path |
|
|
| `OIDC_OP_SIGNING_KEY_PATH` | `data/keys` | OIDC signing key storage |
|
|
| `OIDC_OP_INVITE_TTL` | `86400` | Magic link expiry in seconds |
|
|
| `OIDC_OP_MANAGE_CLIENT_ID` | `manage-app` | Client ID for the management UI |
|
|
| `OIDC_OP_SESSION_HTTPS_ONLY` | `true` | Restrict session cookie to HTTPS (set `false` for local dev) |
|
|
| `OIDC_OP_FORWARD_AUTH_ALLOWED_REDIRECT_HOSTS` | `[]` | JSON list of hosts allowed as post-login return targets for `/forward-auth` |
|
|
| `OIDC_OP_CONFIG_FILE` | `porchlight.toml` | Path to TOML config file |
|
|
|
|
Database migrations run automatically on startup.
|
|
|
|
### Configuration file
|
|
|
|
Copy `porchlight.example.toml` to `porchlight.toml` and edit to suit your
|
|
deployment. The file supports all the same settings as environment variables
|
|
(without the `OIDC_OP_` prefix), plus OIDC client registrations.
|
|
|
|
```toml
|
|
issuer = "https://auth.example.com"
|
|
session_secret = "your-random-secret"
|
|
|
|
[clients.my-webapp]
|
|
client_secret = "change-me-to-a-long-random-string"
|
|
redirect_uris = ["https://app.example.com/callback"]
|
|
response_types = ["code"]
|
|
scope = ["openid", "profile", "email"]
|
|
token_endpoint_auth_method = "client_secret_basic"
|
|
```
|
|
|
|
Each `[clients.<client-id>]` section registers an OIDC Relying Party on
|
|
startup. Only `client_secret` and `redirect_uris` are required; the other
|
|
fields have sensible defaults (`response_types = ["code"]`,
|
|
`scope = ["openid"]`, `token_endpoint_auth_method = "client_secret_basic"`).
|
|
|
|
### Forward-auth for reverse proxies
|
|
|
|
Porchlight exposes `/forward-auth` for proxies such as Traefik, Caddy, and
|
|
nginx `auth_request`. Authenticated active users receive `204 No Content` with
|
|
identity headers (`Remote-User`, `Remote-Email`, `Remote-Groups`, and
|
|
`X-Forwarded-*` equivalents). Anonymous users receive a `303` redirect to the
|
|
Porchlight login page.
|
|
|
|
To redirect users back to a protected app after login, allow that app's host:
|
|
|
|
```toml
|
|
forward_auth_allowed_redirect_hosts = ["app.example.com", "*.apps.example.com"]
|
|
```
|
|
|
|
The endpoint uses `X-Forwarded-Proto`, `X-Forwarded-Host`, and
|
|
`X-Forwarded-Uri` (or `X-Original-Url`) to reconstruct the original request URL.
|
|
Bare host patterns are HTTPS-only and match the default HTTPS port; `host:port`
|
|
patterns are also HTTPS-only unless you include an explicit scheme such as
|
|
`http://localhost:9000` for local cleartext deployments. Exact hosts,
|
|
`host:port` entries, scheme-qualified hosts, and explicit `*.example.com`
|
|
wildcard suffixes listed in `forward_auth_allowed_redirect_hosts` are accepted
|
|
as return targets.
|
|
|
|
Configure the reverse proxy to strip any inbound `Remote-*` and `X-Forwarded-*`
|
|
identity headers from client requests before applying the headers returned by
|
|
`/forward-auth`.
|
|
|
|
To use a config file at a different path:
|
|
|
|
```bash
|
|
export OIDC_OP_CONFIG_FILE=/etc/porchlight/config.toml
|
|
```
|
|
|
|
If the config file does not exist, it is silently ignored and all settings
|
|
fall back to environment variables and defaults.
|
|
|
|
## Development Setup
|
|
|
|
### Prerequisites
|
|
|
|
- Python 3.13+
|
|
- [uv](https://docs.astral.sh/uv/)
|
|
- Node.js (for e2e tests)
|
|
|
|
### Getting started
|
|
|
|
```bash
|
|
# Install dependencies (including dev tools)
|
|
uv sync
|
|
|
|
# Start the dev server with hot reload
|
|
OIDC_OP_ISSUER=http://localhost:8000 OIDC_OP_DEBUG=true \
|
|
uv run uvicorn porchlight.app:create_app \
|
|
--factory --host 127.0.0.1 --port 8000 --reload --reload-dir src
|
|
```
|
|
|
|
Or with Docker:
|
|
|
|
```bash
|
|
docker compose --profile dev up --build
|
|
```
|
|
|
|
### Running tests
|
|
|
|
```bash
|
|
# Unit/integration tests
|
|
uv run pytest
|
|
|
|
# Lint and format
|
|
uv run ruff check src/ tests/ --fix
|
|
uv run ruff format src/ tests/
|
|
|
|
# Type checking
|
|
uv run ty check src/
|
|
```
|
|
|
|
### End-to-end browser tests
|
|
|
|
The e2e suite uses Playwright (Node.js) to test all user-facing flows against a
|
|
running instance of the app.
|
|
|
|
```bash
|
|
# One-time setup: install Playwright and Chromium
|
|
cd tests/e2e
|
|
npm install && npm run setup
|
|
cd ../..
|
|
|
|
# Run all e2e tests
|
|
./tests/e2e/run.sh
|
|
|
|
# Run a specific test
|
|
./tests/e2e/run.sh tests/e2e/test_login.js
|
|
|
|
# Run with visible browser (not headless)
|
|
E2E_HEADLESS=0 ./tests/e2e/run.sh
|
|
```
|
|
|
|
The runner starts the app on port 8099, seeds test fixtures into a temporary
|
|
SQLite database, runs all `test_*.js` files, and tears everything down.
|
|
|
|
### Full quality check
|
|
|
|
```bash
|
|
uv run ruff format src/ tests/
|
|
uv run ruff check src/ tests/ --fix
|
|
uv run ty check src/
|
|
uv run pytest
|
|
./tests/e2e/run.sh
|
|
```
|