From b559133d9ba4aaaa98d00f4f4bdc08c2691d0527 Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Thu, 15 Oct 2026 16:35:39 -0700 Subject: [PATCH] Fix force_refresh_token, the token inspector JWKS link, and dev-key docs Co-Authored-By: Claude Opus 5.5 --- .env.example | 2 +- CLAUDE.md | 11 ++++----- README.md | 45 +++++++++++++++++++++-------------- apps/app/src/index.ts | 2 +- apps/auth/.env.example | 7 +++++- apps/auth/src/index.ts | 6 ++--- apps/shared/README.md | 3 ++- apps/shared/src/validation.ts | 3 ++- 8 files changed, 47 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 22d09e6..5de8a2c 100644 --- a/.env.example +++ b/.env.example @@ -39,7 +39,7 @@ COOKIE_SECRET=dev-cookie-secret-CHANGE-FOR-PRODUCTION API_AUDIENCE=api://my-api # ===== JWKS (Token Signing Keys) ===== -# For development: Leave commented (uses ephemeral keys - tokens invalidate on restart) +# For development: Leave commented (uses oidc-provider's public development key; never deploy without JWKS) # For production: Generate with `node scripts/secrets.js jwks` # JWKS='{"keys":[...]}' diff --git a/CLAUDE.md b/CLAUDE.md index 91a5e86..dd14d97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,8 +101,8 @@ Key features: - **Forced interactions**: Authorization flow uses `prompt: "login consent"` to always show login and consent screens - **RP-initiated logout**: Supports logout with automatic form submission - **Token signing (JWKS)**: - - Development: Uses ephemeral keys (auto-generated on startup with `kid="keystore-CHANGE-ME"`) - - Production: Configure persistent JWKS via `JWKS` environment variable to prevent token invalidation on restarts + - Development: Uses oidc-provider's static, public development key (`kid="keystore-CHANGE-ME"`) + - Production: Configure your own JWKS via `JWKS` environment variable, since the development key is public - Generate keys with: `node scripts/secrets.js jwks` ### 2. Resource Server (API) @@ -230,15 +230,14 @@ The authorization server uses JWKS (JSON Web Key Set) to sign JWT tokens: **Development (default):** - No `JWKS` environment variable needed -- `oidc-provider` auto-generates ephemeral keys on startup +- `oidc-provider` falls back to its public development key - Keys have `kid="keystore-CHANGE-ME"` in JWT headers -- Keys regenerate on each restart (invalidates all tokens) -- Perfectly acceptable for local development +- The private key is public, so this is only acceptable for local development **Production (required):** - Set `JWKS` environment variable with persistent signing keys -- Prevents token invalidation on server restarts +- Keeps the signing key private - Enables proper key rotation strategy - Generate with: `node scripts/secrets.js jwks` - Store securely (AWS Secrets Manager, HashiCorp Vault, etc.) diff --git a/README.md b/README.md index f0d4cbd..08f4b57 100644 --- a/README.md +++ b/README.md @@ -190,13 +190,23 @@ If your browser still complains about certificates, restart it or check your Key Edit the `caddyfile` and add a port to each site: ```caddyfile -:8443 { - tls internal +id.localtest.me:8443 { reverse_proxy localhost:3001 + tls internal +} + +app.localtest.me:8443 { + reverse_proxy localhost:3004 + tls internal +} + +api.localtest.me:8443 { + reverse_proxy localhost:3003 + tls internal } ``` -Then update your `.env` to use `https://localhost:8443` for the issuer and redirect URIs. Port 443 is easier, but this works if you can't use sudo. +Then add `:8443` to every `https://*.localtest.me` URL in your `.env` files (issuer, redirect URIs, and hosts). Port 443 is easier, but this works if you can't use sudo. ### Running the Apps @@ -372,14 +382,14 @@ The tool generates: **Development (default):** -- If you don't set the `JWKS` environment variable, `oidc-provider` will automatically generate ephemeral (temporary) signing keys on startup -- These keys use the default key ID (`kid`) of `"keystore-CHANGE-ME"` -- Keys are regenerated every time the auth server restarts, invalidating all existing tokens -- This is perfectly fine for local development since tokens have short lifetimes (1 hour) +- If you don't set the `JWKS` environment variable, `oidc-provider` signs tokens with its built-in development key +- This key uses the key ID (`kid`) `"keystore-CHANGE-ME"` +- The key is static, and its private half ships in the public `oidc-provider` npm package, so anyone can sign tokens with it +- This is fine for local development, but never for a deployed server **Production (required):** -- You **MUST** provide your own JWKS to prevent token invalidation on server restarts +- You **MUST** provide your own JWKS, since the built-in development key is public - Generate persistent signing keys with `node scripts/secrets.js jwks` - Store the JWKS in a secure environment variable or secret manager - The generated JWKS contains **PRIVATE KEY material** - treat it like any other secret! @@ -388,18 +398,17 @@ The tool generates: When the Authorization Server signs JWT tokens (ID tokens and access tokens), it uses a private key and includes the key ID (`kid`) in the JWT header. The Resource Server (API) uses the public key from the JWKS endpoint to verify token signatures. -**Problems with ephemeral keys in production:** +**Problems with the development key in production:** -1. **Service restarts invalidate all tokens** - Users and applications must re-authenticate -2. **Load balancing issues** - Different servers may have different keys -3. **No key rotation strategy** - Can't implement proper cryptographic key rotation -4. **Debugging difficulties** - The generic `kid` value doesn't help identify which key was used +1. **Anyone can forge tokens** - The private key is published in the `oidc-provider` package +2. **No key rotation strategy** - Can't implement proper cryptographic key rotation +3. **Debugging difficulties** - The generic `kid` value doesn't help identify which key was used **Benefits of persistent keys:** -1. **Tokens survive restarts** - Access/ID tokens remain valid across deployments +1. **Private signing keys** - Only your servers can issue valid tokens 2. **Proper key rotation** - You can add new keys while keeping old ones for validation -3. **Better security** - Control your cryptographic material instead of relying on auto-generated keys +3. **Better security** - Control your cryptographic material instead of relying on a published key 4. **Meaningful key IDs** - Generated keys have unique identifiers like `key-abc123def456` #### Setting Up JWKS for Production @@ -414,7 +423,7 @@ JWKS='{"keys":[{"kty":"RSA","n":"...","e":"AQAB","d":"...","kid":"key-abc123","a The generated JWKS includes: -- **Public components** (`kty`, `n`, `e`, `kid`, `alg`, `use`) - Exposed at `/.well-known/jwks.json` +- **Public components** (`kty`, `n`, `e`, `kid`, `alg`, `use`) - Exposed at `/jwks` - **Private components** (`d`, `p`, `q`, `dp`, `dq`, `qi`) - Used for signing, never exposed **Important notes:** @@ -498,7 +507,7 @@ We use automated tools to keep dependencies secure: This demo intentionally omits some production requirements: -- **Rate limiting** - Prevent brute force and DoS attacks +- **API-wide rate limiting** - Prevent DoS attacks (only the login endpoint is rate-limited) - **Audit logging** - Track authentication events and API access - **Account lockout** - Lock accounts after repeated failed login attempts - **Password hashing** - Replace demo passwords with bcrypt/Argon2 hashed passwords @@ -536,7 +545,7 @@ See `docker-compose.yml` for local development and `docker-compose.prod.example. | Workflow | Purpose | | -------- | ------- | | `ci.yml` | Lint, build, and security audit on PRs | -| `security.yml` | Weekly CodeQL and container scanning | +| `security.yml` | Weekly dependency audit and CodeQL | | `deploy-*.yml` | Deploy services to infrastructure | ### Recommended Branch Protection diff --git a/apps/app/src/index.ts b/apps/app/src/index.ts index 7d92fd1..a1b8f80 100644 --- a/apps/app/src/index.ts +++ b/apps/app/src/index.ts @@ -644,7 +644,7 @@ app.get( "/token", async ( req: Request, res: Response ) => { // Build a validated absolute JWKS URL up front so the view never has to // touch process.env / unvalidated strings inside an `href` attribute. - const jwksUrl = new URL( "/.well-known/jwks.json", ISSUER_URL_OBJ ).href; + const jwksUrl = new URL( "/jwks", ISSUER_URL_OBJ ).href; // Render token inspector view with decoded token data return res.render( "token", { diff --git a/apps/auth/.env.example b/apps/auth/.env.example index 18af8a0..a2c6a40 100644 --- a/apps/auth/.env.example +++ b/apps/auth/.env.example @@ -12,11 +12,16 @@ CLIENT_SECRET=dev-secret-CHANGE-FOR-PRODUCTION REDIRECT_URI=https://app.localtest.me/callback POST_LOGOUT_REDIRECT_URI=https://app.localtest.me +# ===== COOKIE SIGNING ===== +# Development default: dev-cookie-secret-CHANGE-ME +# Production: Generate with: node scripts/secrets.js secrets +COOKIE_SECRET=dev-cookie-secret-CHANGE-FOR-PRODUCTION + # ===== API CONFIGURATION ===== API_AUDIENCE=api://my-api # ===== JWKS (Token Signing Keys) ===== -# Development: Leave commented (uses ephemeral keys) +# Development: Leave commented (uses oidc-provider's public development key) # Production: Generate with: node scripts/secrets.js jwks # JWKS='{"keys":[...]}' diff --git a/apps/auth/src/index.ts b/apps/auth/src/index.ts index 9f06a69..f5c4b5e 100644 --- a/apps/auth/src/index.ts +++ b/apps/auth/src/index.ts @@ -116,11 +116,11 @@ const OIDC_CLIENTS = loadOIDCClients(); logger.info( `Loaded ${ OIDC_CLIENTS.length } OIDC client(s)` ); // Load JWKS (JSON Web Key Set) for token signing with schema validation -// If not provided, oidc-provider will generate ephemeral keys with kid "keystore-CHANGE-ME" +// If not provided, oidc-provider uses its public development key with kid "keystore-CHANGE-ME" function loadJWKS() { const jwksEnv = process.env.JWKS; if ( !jwksEnv ) { - logger.warn( "JWKS not configured - using oidc-provider default ephemeral keys" ); + logger.warn( "JWKS not configured - using oidc-provider's public development key" ); logger.warn( "For production, set JWKS environment variable with your signing keys" ); logger.warn( "Generate keys with: node scripts/secrets.js jwks" ); return undefined; @@ -383,7 +383,7 @@ function validateInteractionUid( uid: string | string[] ): { success: true; data // runtime; we only need TypeScript to keep the values we set well-typed. const configuration = { clients: SANITIZED_CLIENTS, - // Use custom JWKS if provided, otherwise oidc-provider generates ephemeral keys + // Use custom JWKS if provided, otherwise oidc-provider uses its public development key ...( JWKS ? { jwks: JWKS } : {} ), claims: { openid: [ "sub" ], diff --git a/apps/shared/README.md b/apps/shared/README.md index 98624dd..61dd09c 100644 --- a/apps/shared/README.md +++ b/apps/shared/README.md @@ -94,7 +94,8 @@ pnpm clean ## Dependencies -- **Peer Dependencies**: `pino` (logging) +- **Dependencies**: `pino` (logging), `zod`, `helmet` +- **Peer Dependencies**: `express` - **Dev Dependencies**: `typescript`, `@types/node` ## License diff --git a/apps/shared/src/validation.ts b/apps/shared/src/validation.ts index 1358d2a..1a0a275 100644 --- a/apps/shared/src/validation.ts +++ b/apps/shared/src/validation.ts @@ -284,7 +284,8 @@ export const oidcClientSchema = z.object( { "client_secret_basic", "client_secret_post", "none" - ] ).optional() + ] ).optional(), + force_refresh_token: z.boolean().optional() } ); export const oidcClientsSchema = z.array( oidcClientSchema );