What ships
The full auth-code + PKCE flow:| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/.well-known/openid-configuration | GET | none | OIDC discovery document |
/oidc/jwks | GET | none | RS256 public key (auto-generated, persisted) |
/oidc/authorize | GET | session cookie | Initiate auth-code flow |
/oidc/token | POST | client_id + secret + PKCE | Exchange code → id_token + access_token |
/oidc/userinfo | GET | Bearer access_token | Standard OIDC user claims |
Security stance
- PKCE S256 is REQUIRED for every authorize request —
plainis rejected per OAuth 2.1, no-PKCE is rejected outright. - redirect_uri must match the client’s registered list by exact string compare (no path coercion, suffix matching, or scheme upgrades — the textbook OIDC open-redirect footgun).
- client_id + client_secret are constant-time compared. Public clients (no secret in registration) authenticate via PKCE alone.
- id_token claims:
iss,sub,aud,exp(10 min),iat,nonce(when supplied at /authorize),email,email_verified,name(subject to requested scopes). - access_token is opaque random (32-byte base64url), TTL 1 hour.
- refresh_token is NOT issued. Clients re-do the auth code flow when the access_token expires — keeps the surface small and drops a class of long-lived bearer leak.
Configuration
PYLON_OIDC_CLIENTS is a JSON array of {client_id, client_secret?, redirect_uris[]}. client_secret is optional — omit it for SPAs / native apps that authenticate via PKCE only.
Signing key
On first start (withPYLON_OIDC_ISSUER set), Pylon generates a 2048-bit RSA key, persists it as PKCS#8 PEM at PYLON_OIDC_KEY_PATH, and chmods the file to 0600 so it isn’t world-readable. Same key reused across restarts so issued id_tokens stay verifiable.
The JWKS endpoint publishes the matching public key with:
kid= first 16 hex chars of SHA-256(modulus) — stable across restarts, changes on rotationkty: "RSA",alg: "RS256",use: "sig"n+e= base64url-no-pad-encoded big-endian integers
Auth-code flow
A typical end-to-end exchange:Userinfo claim projection
/oidc/userinfo projects the User row through the same auth.user.expose / auth.user.hide config that protects /api/auth/session from leaking secrets. passwordHash and underscore-prefixed fields never reach a downstream service through userinfo.
Claims returned per scope:
| Scope | Claims |
|---|---|
openid | sub |
email | email, email_verified |
profile | name (from displayName or name) |
Where to go next
- JWT sessions — minting the tokens app-internal services verify
- SSO — the other direction (external IdPs signing INTO Pylon)
- Sessions — the cookie that gates
/oidc/authorize