Add active-user checks to password login, WebAuthn login, and magic link registration to prevent deactivated accounts from authenticating. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .idea | ||
| data/keys | ||
| docs/plans | ||
| src/porchlight | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Makefile | ||
| porchlight.example.toml | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
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)
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:
OIDC_OP_ISSUER=https://auth.example.com
OIDC_OP_SESSION_SECRET=<random-secret>
Manual
Requires Python 3.13+ and uv.
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:
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.
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.
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_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.
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").
To use a config file at a different path:
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
- Node.js (for e2e tests)
Getting started
# 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:
docker compose --profile dev up --build
Running tests
# 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.
# 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
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