OpenID Connect Provider with user management, built with FastAPI.
Find a file
2026-03-10 09:00:11 +01:00
.idea rename package directory fastapi_oidc_op → porchlight 2026-02-16 15:29:31 +01:00
data/keys chore: stop tracking data/keys/public_jwks.json 2026-02-20 15:44:18 +01:00
docs/plans docs: add profile validation implementation plan 2026-02-20 15:05:00 +01:00
src/porchlight feat: add logout buttons to admin and manage navigation bars 2026-02-20 15:41:45 +01:00
tests feat: add ProfileUpdate pydantic model with email and phone validation 2026-02-20 15:21:28 +01:00
.dockerignore fix: add session_https_only to dev config and update README 2026-02-19 15:10:37 +01:00
.gitignore update .gitignore 2026-03-10 09:00:11 +01:00
docker-compose.yml fix: add session_https_only to dev config and update README 2026-02-19 15:10:37 +01:00
Dockerfile fix: add session_https_only to dev config and update README 2026-02-19 15:10:37 +01:00
Makefile add Makefile 2026-02-20 15:04:04 +01:00
porchlight.example.toml docs: add example config file and update README 2026-02-18 12:54:43 +01:00
pyproject.toml feat: add ProfileUpdate pydantic model with email and phone validation 2026-02-20 15:21:28 +01:00
README.md fix: add session_https_only to dev config and update README 2026-02-19 15:10:37 +01:00
uv.lock feat: add ProfileUpdate pydantic model with email and phone validation 2026-02-20 15:21:28 +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_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