Skip to main content
Pylon can be the identity provider other systems sign into through. Apps that ship with their own internal tools, microservices, or third-party SaaS that accepts “any OIDC IdP” point at Pylon’s discovery doc and treat the id_tokens Pylon issues as the identity layer.

What ships

The full auth-code + PKCE flow:
EndpointMethodAuthPurpose
/.well-known/openid-configurationGETnoneOIDC discovery document
/oidc/jwksGETnoneRS256 public key (auto-generated, persisted)
/oidc/authorizeGETsession cookieInitiate auth-code flow
/oidc/tokenPOSTclient_id + secret + PKCEExchange code → id_token + access_token
/oidc/userinfoGETBearer access_tokenStandard OIDC user claims

Security stance

  • PKCE S256 is REQUIRED for every authorize request — plain is 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_ISSUER=https://auth.your-app.com
PYLON_OIDC_CLIENTS='[{"client_id":"docs-portal","client_secret":"shh-1234","redirect_uris":["https://docs.example.com/oauth/callback"]}]'

# Optional — defaults to <data_dir>/oidc-signing-key.pem with 0600 perms
PYLON_OIDC_KEY_PATH=/var/lib/pylon/oidc-signing-key.pem

# Optional — defaults to /login, the dashboard's own login page
PYLON_LOGIN_URL=/login
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 (with PYLON_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 rotation
  • kty: "RSA", alg: "RS256", use: "sig"
  • n + e = base64url-no-pad-encoded big-endian integers
curl https://auth.your-app.com/.well-known/openid-configuration
curl https://auth.your-app.com/oidc/jwks

Auth-code flow

A typical end-to-end exchange:
# 1. Client redirects user's browser to /authorize.
https://auth.your-app.com/oidc/authorize?
  response_type=code
  &client_id=docs-portal
  &redirect_uri=https://docs.example.com/oauth/callback
  &scope=openid+email+profile
  &state=<csrf-token>
  &nonce=<id-token-binding>
  &code_challenge=<S256(verifier)>
  &code_challenge_method=S256

# 2. User logs in (Pylon's /login handles whichever method they use).
#    Pylon redirects browser back to redirect_uri with ?code=...&state=...

# 3. Client exchanges code at /token.
POST /oidc/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<auth-code>
&redirect_uri=https://docs.example.com/oauth/callback
&client_id=docs-portal
&client_secret=shh-1234
&code_verifier=<original-PKCE-verifier>

# 200 OK
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "<RS256-signed JWT>",
  "scope": "openid email profile"
}

# 4. Client calls /userinfo with the access_token.
GET /oidc/userinfo
Authorization: Bearer <access_token>

# 200 OK
{
  "sub": "user_abc123",
  "email": "[email protected]",
  "email_verified": true,
  "name": "Alice Liddell"
}

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:
ScopeClaims
openidsub
emailemail, email_verified
profilename (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