OpenID Connect Provider with user management, built with FastAPI.
Find a file
Johan Lundberg 202b67d93e
All checks were successful
publish-latest / docker (push) Successful in 12s
updated logo
2026-07-19 23:54:11 +02:00
.agents/skills/beads more agent config, mostly about beads 2026-06-29 09:22:50 +02:00
.claude more agent config, mostly about beads 2026-06-29 09:22:50 +02:00
.codex more agent config, mostly about beads 2026-06-29 09:22:50 +02:00
.forgejo/workflows setup registry secret 2026-07-01 11:49:56 +02:00
docs/plans add uncommitted plans and CLAUDE.md 2026-04-10 11:28:51 +02:00
examples/rp-reference reference RP 2026-06-29 09:23:22 +02:00
src/porchlight updated logo 2026-07-19 23:54:11 +02:00
tests please ruff 2026-07-05 00:10:59 +02:00
.dockerignore fix: add session_https_only to dev config and update README 2026-02-19 15:10:37 +01:00
.gitignore ignore beads and dolt 2026-06-29 09:18:07 +02:00
AGENTS.md more agent config, mostly about beads 2026-06-29 09:22:50 +02:00
CLAUDE.md update CLAUDE.md 2026-06-29 09:18:37 +02:00
docker-compose.yml always require OIDC_OP_ISSUER and OIDC_OP_SESSION_SECRET to be set when using compose 2026-07-19 23:18:27 +02:00
Dockerfile create unprivileged user 2026-07-19 23:17:54 +02:00
Makefile add Makefile 2026-02-20 15:04:04 +01:00
porchlight.dev.toml Implement reverse-proxy forward-auth 2026-07-04 23:29:26 +02:00
porchlight.example.toml Implement reverse-proxy forward-auth 2026-07-04 23:29:26 +02:00
pyproject.toml build: add slowapi and zxcvbn dependencies 2026-03-25 13:46:55 +01:00
README.md Harden forward-auth redirect and header handling 2026-07-05 00:00:58 +02:00
ruff.toml added ruff config 2026-03-31 15:40:37 +02:00
uv.lock build: add slowapi and zxcvbn dependencies 2026-03-25 13:46:55 +01:00

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 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_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.

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:

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:

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