OpenID Connect Provider with user management, built with FastAPI.
Find a file
2026-02-19 11:35:25 +01:00
.idea rename package directory fastapi_oidc_op → porchlight 2026-02-16 15:29:31 +01:00
data/keys feat: update login UI and JS for usernameless WebAuthn authentication 2026-02-17 13:42:35 +01:00
docs/plans docs: add consent screen design 2026-02-18 13:50:56 +01:00
scripts chore: add quality check script (ruff, ty, pytest) 2026-02-12 15:25:00 +01:00
src/porchlight feat: add admin user list page with search and pagination 2026-02-19 11:35:25 +01:00
tests feat: add admin router with admin group guard 2026-02-19 11:18:50 +01:00
.dockerignore feat: add Docker support with multi-stage build and compose profiles 2026-02-16 14:59:50 +01:00
.gitignore refactor: fix lint warnings and remove stale type: ignore comments 2026-02-18 13:08:03 +01:00
docker-compose.yml fix: bind-mount README.md in dev container for hatchling build 2026-02-16 15:07:51 +01:00
Dockerfile update Dockerfile: fastapi_oidc_op → porchlight 2026-02-16 15:35:21 +01:00
porchlight.example.toml docs: add example config file and update README 2026-02-18 12:54:43 +01:00
pyproject.toml update pyproject.toml: rename to porchlight, add typer dependency 2026-02-16 15:31:46 +01:00
README.md docs: add example config file and update README 2026-02-18 12:54:43 +01:00
uv.lock update pyproject.toml: rename to porchlight, add typer dependency 2026-02-16 15:31:46 +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_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