# 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 = "" 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="" 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 . 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 |