> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OIDC provider

> Pylon as a full OpenID Connect identity provider — auth-code flow with PKCE, RS256 id_tokens, JWKS, userinfo.

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:

| 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 — `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

```bash theme={null}
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

```bash theme={null}
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": "alice@example.com",
  "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:

| Scope     | Claims                                |
| --------- | ------------------------------------- |
| `openid`  | `sub`                                 |
| `email`   | `email`, `email_verified`             |
| `profile` | `name` (from `displayName` or `name`) |

## Where to go next

* **[JWT sessions](/auth/jwt)** — minting the tokens app-internal services verify
* **[SSO](/auth/sso)** — the other direction (external IdPs signing INTO Pylon)
* **[Sessions](/auth/sessions)** — the cookie that gates `/oidc/authorize`
