porchlight/examples/rp-reference/README.md
Johan Lundberg 8e8c33a407
reference RP
2026-06-29 09:23:22 +02:00

114 lines
4 KiB
Markdown

# Porchlight OIDC RP — reference implementation
A small, standalone [Relying Party](https://openid.net/specs/openid-connect-core-1_0.html)
that authenticates users against a running **porchlight** OpenID Provider.
It hand-rolls every OIDC step (no OIDC client library) so the protocol is
readable. `PyJWT` is used only for the RS256 signature primitive and JWK
parsing — the OIDC claim checks (`iss` / `aud` / `exp` / `nonce`) are written
out explicitly in [`oidc_client.py`](./oidc_client.py).
> Not part of porchlight's shipped code. Lives here as a teaching reference and
> is intentionally left untracked by git.
## What it demonstrates
1. **Discovery** — fetching `/.well-known/openid-configuration`.
2. **Authorization Code flow with PKCE** (S256), plus `state` and `nonce`.
3. **Token exchange** using `client_secret_basic` auth.
4. **ID token verification** — signature via JWKS, then explicit claim checks.
5. **UserInfo** — calling the endpoint with the access token.
6. **Refresh** — using the refresh token (requires `offline_access`).
7. **Logout** — local only. Porchlight exposes no `end_session_endpoint`, so
global single-logout at the OP is not possible; see the comment in `app.py`.
## 1. Register this RP in porchlight
Generate a client secret:
```bash
python -c "import secrets; print(secrets.token_urlsafe(48))"
```
Add a client to your `porchlight.toml` (paste the secret above):
```toml
[clients.showcase-rp]
client_secret = "<paste-generated-secret>"
redirect_uris = ["http://localhost:9000/callback"]
response_types = ["code"]
scope = ["openid", "profile", "email", "offline_access"]
token_endpoint_auth_method = "client_secret_basic"
```
Restart porchlight so it picks up the new client.
## 2. Run porchlight (OP) on :8000
From the repo root:
```bash
OIDC_OP_ISSUER=http://localhost:8000 OIDC_OP_DEBUG=true \
OIDC_OP_CONFIG_FILE=porchlight.toml \
uv run uvicorn porchlight.app:create_app \
--factory --host 127.0.0.1 --port 8000 --reload --reload-dir src
```
Make sure you have at least one user to log in as (see the porchlight CLI:
`uv run porchlight initial-admin` / `create-invite`).
## 3. Run this RP on :9000
```bash
cd examples/rp-reference
# tell the RP the client secret you generated in step 1
export OIDC_RP_CLIENT_SECRET="<paste-generated-secret>"
uv run --with-requirements <(uv pip compile pyproject.toml 2>/dev/null) \
uvicorn app:create_app --factory --port 9000 --reload
```
Or, more simply, let uv resolve the local project:
```bash
cd examples/rp-reference
uv run uvicorn app:create_app --factory --port 9000 --reload
```
## 4. Walk through the flow
1. Open <http://localhost:9000/>.
2. Click **Login with Porchlight** → you're redirected to porchlight.
3. Authenticate and approve the consent screen.
4. You land back on the result page showing:
- the **verified ID token claims**,
- the **UserInfo** response,
- the **raw token response**.
5. Click **Refresh tokens** — note the access token changes and the refresh
token rotates, but no new `id_token` is issued (porchlight does not re-mint
ID tokens on refresh).
6. Click **Log out** — clears this RP's session only.
## Configuration
All optional; defaults assume localhost. Override with env vars:
| Variable | Default | Meaning |
|---|---|---|
| `OIDC_RP_ISSUER` | `http://localhost:8000` | porchlight issuer URL |
| `OIDC_RP_CLIENT_ID` | `showcase-rp` | must match the `porchlight.toml` client |
| `OIDC_RP_CLIENT_SECRET` | `change-me` | the generated secret |
| `OIDC_RP_REDIRECT_URI` | `http://localhost:9000/callback` | must match a registered redirect_uri |
| `OIDC_RP_SCOPE` | `openid profile email offline_access` | requested scopes |
| `OIDC_RP_SESSION_SECRET` | dev default | signs the RP session cookie (not the OIDC secret) |
| `OIDC_RP_LEEWAY` | `30` | allowed clock skew (s) for exp/iat |
## Files
| File | Purpose |
|---|---|
| `oidc_client.py` | the hand-rolled OIDC steps — start reading here |
| `app.py` | FastAPI routes + in-memory session handling |
| `templates.py` | minimal inline HTML |
| `config.py` | env-var configuration |