Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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":[...]}'

Expand Down
11 changes: 5 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.)
Expand Down
45 changes: 27 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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!
Expand All @@ -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
Expand All @@ -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:**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
7 changes: 6 additions & 1 deletion apps/auth/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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":[...]}'

Expand Down
6 changes: 3 additions & 3 deletions apps/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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" ],
Expand Down
3 changes: 2 additions & 1 deletion apps/shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/shared/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
Loading