diff --git a/content/configuration/jwt_algorithms.md b/content/configuration/jwt_algorithms.md new file mode 100644 index 0000000..5a0d6d2 --- /dev/null +++ b/content/configuration/jwt_algorithms.md @@ -0,0 +1,410 @@ +--- +title: 'JWT Signing Algorithms' +weight: 6 +--- + +# JWT Signing Algorithms + +Uitsmijter supports two JWT signing algorithms for access tokens: **HS256** (HMAC with SHA-256) and **RS256** (RSA with SHA-256). This guide explains the differences between these algorithms and how to migrate from HS256 to RS256. + +## Algorithm Comparison + +| Feature | HS256 (Symmetric) | RS256 (Asymmetric) | +|---------|-------------------|-------------------| +| **Key Type** | Shared secret (single key) | RSA key pair (public + private) | +| **Security** | Good (if secret is protected) | **Better** (private key never shared) | +| **Key Distribution** | Secret must be shared securely | Public key can be distributed openly | +| **Token Verification** | Requires shared secret | Uses public key (via JWKS) | +| **Key Rotation** | Requires secret update everywhere | **Seamless** (JWKS supports multiple keys) | +| **Performance** | Faster (symmetric crypto) | Slightly slower (asymmetric crypto) | +| **Use Case** | Simple deployments, testing | **Production, microservices** | +| **JWKS Endpoint** | Not used | **Required** (`/.well-known/jwks.json`) | +| **Default** | Yes (backward compatibility) | Recommended for new deployments | + +## HS256 (HMAC with SHA-256) + +HS256 is a symmetric algorithm that uses a shared secret key to both sign and verify JWTs. + +### How it works + +1. Uitsmijter signs JWTs using a secret key (from `JWT_SECRET` environment variable) +2. Resource servers verify JWTs using the **same secret key** +3. The secret must be securely shared between Uitsmijter and all resource servers + +### Configuration + +HS256 is the default algorithm for backward compatibility: + +```yaml +# .env or deployment config +JWT_ALGORITHM: HS256 # or omit this line entirely (defaults to HS256) +JWT_SECRET: your-secret-key-at-least-256-bits +``` + +### When to use HS256 + +- **Development and testing**: Simple setup, no key management +- **Monolithic applications**: Single application verifies tokens +- **Legacy systems**: Already using HS256 and shared secrets +- **High-performance scenarios**: Marginally faster than RS256 + +### Security considerations + +- **Secret management**: The `JWT_SECRET` must be kept confidential +- **Secret distribution**: Every service that verifies tokens needs the secret +- **Key rotation**: Rotating keys requires updating all services simultaneously +- **Compromise risk**: If one service is compromised, the secret is exposed + +## RS256 (RSA with SHA-256) + +RS256 is an asymmetric algorithm that uses an RSA key pair: a private key for signing and a public key for verification. + +### How it works + +1. Uitsmijter generates an RSA key pair (2048-bit) +2. Uitsmijter signs JWTs with the **private key** (kept secret) +3. Uitsmijter publishes the **public key** via the JWKS endpoint (`/.well-known/jwks.json`) +4. Resource servers fetch the public key from JWKS +5. Resource servers verify JWTs using the public key (no secrets needed) + +### Configuration + +Enable RS256 by setting the `JWT_ALGORITHM` environment variable: + +```yaml +# .env or deployment config +JWT_ALGORITHM: RS256 +``` + +That's it! Uitsmijter will automatically: +- Generate RSA key pairs on startup +- Publish public keys at `/.well-known/jwks.json` +- Include `kid` (Key ID) in JWT headers +- Support key rotation + +You do **not** need to manually generate or manage RSA keys. + +### When to use RS256 (Recommended) + +- **Production deployments**: Superior security and key management +- **Microservices architecture**: Each service can verify tokens independently +- **Multi-tenant systems**: Different tenants can have different keys +- **Compliance requirements**: Many standards require asymmetric signing +- **Key rotation**: Seamless rotation without service disruption + +### Security advantages + +- **Private key protection**: Private keys never leave Uitsmijter +- **Public key distribution**: Public keys can be shared openly (via JWKS) +- **No shared secrets**: Resource servers don't need confidential data +- **Key rotation**: Old keys remain in JWKS during grace period +- **Compromise mitigation**: Compromising a resource server doesn't expose signing keys + +## Migrating from HS256 to RS256 + +### Zero-Downtime Migration Strategy + +This migration strategy allows you to switch from HS256 to RS256 without invalidating existing tokens or causing downtime. + +#### Step 1: Understand the impact + +**What changes:** +- JWT signing algorithm changes from HS256 to RS256 +- JWT header includes `kid` field for key identification +- Public keys become available at `/.well-known/jwks.json` +- Resource servers must fetch public keys from JWKS (instead of using shared secret) + +**What stays the same:** +- JWT payload structure (claims remain unchanged) +- Token expiration times +- OAuth endpoints and flows +- Client applications (if using standard OAuth libraries) + +#### Step 2: Update resource servers first + +Before switching Uitsmijter to RS256, update all resource servers to support JWKS-based verification. Most JWT libraries support this with minimal changes. + +**Example: Node.js with `jsonwebtoken` and `jwks-rsa`** + +Before (HS256): +```javascript +import jwt from 'jsonwebtoken'; + +const secret = process.env.JWT_SECRET; + +// Verify token +jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => { + // ... +}); +``` + +After (RS256 with JWKS): +```javascript +import jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; + +const client = jwksClient({ + jwksUri: 'https://id.example.com/.well-known/jwks.json', + cache: true, + cacheMaxAge: 3600000 // 1 hour +}); + +function getKey(header, callback) { + client.getSigningKey(header.kid, (err, key) => { + const signingKey = key.getPublicKey(); + callback(null, signingKey); + }); +} + +// Verify token (works with both HS256 and RS256) +jwt.verify(token, getKey, { algorithms: ['HS256', 'RS256'] }, (err, decoded) => { + // ... +}); +``` + +**Key points:** +- Keep `HS256` in the `algorithms` array temporarily (supports both algorithms) +- The JWKS client will automatically fetch and cache public keys +- Works with HS256 tokens (falls back to cached secret) and RS256 tokens (uses JWKS) + +#### Step 3: Deploy updated resource servers + +Deploy the updated resource servers that support JWKS. Verify that they can still validate existing HS256 tokens. + +Test with a sample HS256 token: +```bash +curl -H "Authorization: Bearer YOUR_HS256_TOKEN" https://your-api.example.com/protected +``` + +The request should succeed, confirming backward compatibility. + +#### Step 4: Switch Uitsmijter to RS256 + +Update Uitsmijter's configuration to use RS256: + +**Kubernetes/Helm:** +```yaml +# values.yaml +env: + JWT_ALGORITHM: RS256 +``` + +**Docker Compose:** +```yaml +environment: + - JWT_ALGORITHM=RS256 +``` + +**Direct deployment:** +```bash +export JWT_ALGORITHM=RS256 +``` + +#### Step 5: Restart Uitsmijter + +Restart Uitsmijter to apply the new configuration: + +```bash +# Kubernetes +kubectl rollout restart deployment/uitsmijter + +# Docker Compose +docker-compose restart uitsmijter +``` + +Uitsmijter will: +1. Generate a new RSA key pair on startup +2. Start signing new JWTs with RS256 +3. Publish the public key at `/.well-known/jwks.json` + +#### Step 6: Verify RS256 tokens + +Test that new tokens are signed with RS256: + +1. Obtain a new access token: +```bash +# Use your OAuth flow to get a new token +curl -X POST https://id.example.com/token \ + -d grant_type=authorization_code \ + -d code=YOUR_CODE \ + -d client_id=YOUR_CLIENT_ID +``` + +2. Decode the JWT header (without verifying): +```bash +# Extract and decode the header +echo "YOUR_TOKEN" | cut -d'.' -f1 | base64 -d +``` + +Expected output: +```json +{ + "alg": "RS256", + "typ": "JWT", + "kid": "2024-11-08" +} +``` + +3. Verify the token works with your resource servers: +```bash +curl -H "Authorization: Bearer YOUR_RS256_TOKEN" https://your-api.example.com/protected +``` + +#### Step 7: Wait for HS256 tokens to expire + +Old HS256 tokens remain valid until they expire (typically 2 hours by default). During this grace period: +- New tokens are signed with RS256 +- Old HS256 tokens continue to work +- Resource servers support both algorithms + +**Monitor token expiration:** +```bash +# Check when the last HS256 token will expire +# Default token lifetime is 2 hours +``` + +#### Step 8: Remove HS256 support (optional) + +After all HS256 tokens have expired (wait at least `TOKEN_EXPIRATION_IN_HOURS` × 2), you can remove HS256 support from resource servers: + +```javascript +// Remove 'HS256' from algorithms array +jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => { + // Now only accepts RS256 tokens +}); +``` + +You can also remove the `JWT_SECRET` environment variable from resource servers (no longer needed). + +### Rollback Strategy + +If you encounter issues during migration, you can rollback to HS256: + +1. Change `JWT_ALGORITHM` back to `HS256` (or remove it) +2. Restart Uitsmijter +3. New tokens will be signed with HS256 again +4. RS256 tokens issued during the RS256 period will fail verification after rollback + +**Important:** Plan the migration during a maintenance window or low-traffic period to minimize impact. + +## Key Rotation (RS256 only) + +With RS256, you can rotate signing keys without downtime: + +### Manual key rotation + +1. Generate a new key by restarting Uitsmijter +2. The new key gets a new `kid` (current date: `YYYY-MM-DD`) +3. New JWTs are signed with the new key +4. Old public keys remain in JWKS for verification +5. After grace period, old keys can be removed from JWKS + +### Automatic key rotation + +Uitsmijter doesn't currently implement automatic key rotation, but you can implement it using: + +1. **Scheduled restarts**: Restart Uitsmijter monthly/quarterly (generates new key) +2. **External key management**: Use Kubernetes secrets rotation +3. **Manual rotation**: Generate new key via admin endpoint (future feature) + +### Best practices for key rotation + +- **Grace period**: Keep old keys in JWKS for at least 2× token lifetime +- **Monitoring**: Monitor JWT verification failures during rotation +- **Documentation**: Document which `kid` is active at any time +- **Testing**: Test rotation in staging before production + +## Troubleshooting + +### "Invalid signature" errors after switching to RS256 + +**Cause**: Resource servers are still trying to verify RS256 tokens with HS256 secret. + +**Solution**: Ensure resource servers are updated to use JWKS (Step 2 of migration guide). + +### JWKS endpoint returns empty `keys` array + +**Cause**: `JWT_ALGORITHM` is still set to HS256 or not set. + +**Solution**: Verify `JWT_ALGORITHM=RS256` is set and restart Uitsmijter. + +### "kid not found in JWKS" errors + +**Cause**: Resource server's JWKS cache is stale, or key was rotated. + +**Solution**: +- Clear JWKS cache (most libraries auto-refresh) +- Verify JWKS endpoint contains the `kid` from the JWT header +- Check that clocks are synchronized (NTP) + +### Performance degradation after switching to RS256 + +**Cause**: RS256 is slightly slower than HS256 (asymmetric crypto overhead). + +**Solution**: +- Enable JWKS caching in resource servers (default: 1 hour) +- Use CDN or caching proxy for JWKS endpoint +- Consider increasing token expiration time to reduce token issuance frequency + +### Resource server can't reach JWKS endpoint + +**Cause**: Network policy, firewall, or DNS issue. + +**Solution**: +- Verify resource server can reach `https://id.example.com/.well-known/jwks.json` +- Check network policies allow outbound HTTPS +- Use internal DNS or service discovery if applicable + +## Environment Variables + +### JWT_ALGORITHM + +Controls the JWT signing algorithm. + +**Values:** +- `HS256` (default): HMAC with SHA-256 (symmetric) +- `RS256`: RSA with SHA-256 (asymmetric) + +**Example:** +```yaml +JWT_ALGORITHM: RS256 +``` + +### JWT_SECRET + +(HS256 only) The shared secret used for HS256 signing. + +**Requirements:** +- Minimum 256 bits (32 characters) +- Must be kept confidential +- Must match on all services verifying tokens + +**Example:** +```yaml +JWT_SECRET: your-secret-key-at-least-32-characters-long +``` + +**Not used when `JWT_ALGORITHM=RS256`**. + +### TOKEN_EXPIRATION_IN_HOURS + +Controls JWT access token expiration time. + +**Default:** `2` (2 hours) + +**Example:** +```yaml +TOKEN_EXPIRATION_IN_HOURS: 8 +``` + +Affects: +- Access token lifetime +- Grace period for key rotation (should be 2× this value) + +## Further Reading + +- [RFC 7517 - JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517) +- [RFC 7518 - JSON Web Algorithms (JWA)](https://www.rfc-editor.org/rfc/rfc7518) +- [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) +- [Available Endpoints](/oauth/endpoints) +- [JWT Decoding](/oauth/jwt_decoding) diff --git a/content/oauth/endpoints.md b/content/oauth/endpoints.md index a50d5a5..5fff060 100644 --- a/content/oauth/endpoints.md +++ b/content/oauth/endpoints.md @@ -171,6 +171,93 @@ used in certain grant types. The refresh_token is used to obtain a new access to without having to prompt the user for their login credentials again. That is strictly forbidden with the password grant type. +### /revoke + +The `/revoke` endpoint allows clients to notify Uitsmijter that a previously obtained token (access token or refresh token) is no longer needed and should be invalidated. This endpoint implements [RFC 7009: OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009). + +**Why use token revocation?** + +- **Security**: Proactively invalidate tokens when they're no longer needed (user logs out, app uninstalled) +- **Privacy**: Allow users to revoke access granted to third-party applications +- **Best Practice**: Recommended by OAuth 2.0 Security Best Current Practice + +**Request format:** + +The revocation endpoint accepts HTTP POST requests with `application/x-www-form-urlencoded` content type: + +```shell +curl --request POST \ + --url https://id.example.com/revoke \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data 'token=V7vZQbJNNY7zR8IWyV7vZQbJNNY7zR8IW' \ + --data 'token_type_hint=access_token' \ + --data 'client_id=9095A4F2-35B2-48B1-A325-309CA324B97E' \ + --data 'client_secret=secret123' +``` + +**Parameter description:** + +| Parameter | Required | Description | +|-----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| token | Yes | The token value to revoke. Can be either an access token (JWT) or refresh token. | +| token_type_hint | No | Hint about the token type: `access_token` or `refresh_token`. Helps the server optimize token lookup. If the hint is incorrect, the server will search across all token types. | +| client_id | Yes | The unique identifier of the client application making the revocation request. | +| client_secret | Optional | The client secret. REQUIRED for confidential clients (clients with a configured secret). MUST NOT be provided for public clients. The same authentication rules as the token endpoint apply. | + +**Response:** + +Per RFC 7009, the authorization server responds with HTTP 200 OK regardless of whether the token was valid, already revoked, or never existed. This prevents information disclosure about token validity. + +```http +HTTP/1.1 200 OK +``` + +If client authentication fails, the server returns HTTP 401 Unauthorized: + +```http +HTTP/1.1 401 Unauthorized +Content-Type: application/json + +{ + "error": "invalid_client", + "error_description": "ERROR.INVALID_CLIENT" +} +``` + +**Token ownership validation:** + +The server validates that the token belongs to the requesting client before revoking it. If a client tries to revoke a token that belongs to a different client, the revocation is silently ignored (returns 200 OK without revoking the token). + +**Cascading revocation:** + +When revoking a refresh token, Uitsmijter also revokes the associated authorization code. This prevents the client from using the authorization code to obtain new tokens after the refresh token has been revoked. + +> **Note about JWT access tokens**: Access tokens issued by Uitsmijter are stateless JWTs. While the revocation endpoint accepts and validates access tokens, it cannot truly invalidate them before their expiration time. Future enhancements may include a token blacklist or reduced token lifetimes for revoked tokens. + +**Example usage in an application:** + +```javascript +// When user logs out, revoke the refresh token +async function logout(refreshToken, clientId, clientSecret) { + await fetch('https://id.example.com/revoke', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + token: refreshToken, + token_type_hint: 'refresh_token', + client_id: clientId, + client_secret: clientSecret + }) + }); + + // Clear local session + localStorage.removeItem('refresh_token'); + localStorage.removeItem('access_token'); +} +``` + ## Discovery endpoints Discovery endpoints allow OAuth/OpenID Connect clients to automatically discover the configuration and capabilities of Uitsmijter without manual configuration. This is especially useful for dynamic client registration, multi-tenant deployments, and maintaining compatibility across different versions. @@ -210,6 +297,7 @@ This returns a JSON document with the OpenID Provider Metadata: "token_endpoint": "https://id.example.com/token", "userinfo_endpoint": "https://id.example.com/userinfo", "jwks_uri": "https://id.example.com/.well-known/jwks.json", + "revocation_endpoint": "https://id.example.com/revoke", "scopes_supported": ["openid", "profile", "email", "read", "write"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], @@ -258,6 +346,151 @@ The library will automatically fetch the discovery document and configure itself > **Note**: The discovery endpoint is publicly accessible and does not require authentication. This is by design, as clients need to discover the configuration before they can authenticate. +### /.well-known/jwks.json + +The `/.well-known/jwks.json` endpoint provides the JSON Web Key Set (JWKS) containing public keys used to verify JWT signatures. This endpoint implements [RFC 7517 (JSON Web Key)](https://www.rfc-editor.org/rfc/rfc7517) and is essential for clients that need to verify JWT access tokens signed with asymmetric algorithms. + +**Why use JWKS?** + +When Uitsmijter signs JWTs with the RS256 algorithm (RSA with SHA-256), clients need access to the public key to verify the JWT signature. The JWKS endpoint provides these public keys in a standardized JSON format that can be: + +1. **Automatically fetched**: OAuth libraries can automatically download and cache public keys +2. **Rotated safely**: Uitsmijter can rotate signing keys while keeping old keys available during a grace period +3. **Multi-key support**: Multiple active keys can coexist, identified by their `kid` (Key ID) +4. **Standards-compliant**: Works with all standard JWT libraries and validators + +**When is JWKS used?** + +- **RS256 JWT verification**: When Uitsmijter is configured with `JWT_ALGORITHM=RS256` (recommended for production) +- **Token validation**: Resource servers verify access token signatures without contacting Uitsmijter +- **Stateless authentication**: JWTs can be validated entirely client-side using the public key + +**Example**: Fetching the JWKS + +```shell +curl --request GET \ + --url https://id.example.com/.well-known/jwks.json \ + --header 'Accept: application/json' +``` + +This returns a JSON Web Key Set containing one or more RSA public keys: + +```json +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "2024-11-08", + "alg": "RS256", + "n": "0vx7agoebGcQSu...V_3Qb", + "e": "AQAB" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "2024-11-01", + "alg": "RS256", + "n": "xjlCRBqkO8WJ...kQb", + "e": "AQAB" + } + ] +} +``` + +**JWKS Response fields:** + +| Field | Description | +|-------|-------------| +| `keys` | Array of JSON Web Keys. Multiple keys support key rotation. | +| `kty` | Key Type. Always "RSA" for Uitsmijter's asymmetric keys. | +| `use` | Public Key Use. Always "sig" (signature verification). | +| `kid` | Key ID. Matches the `kid` field in JWT headers for key selection. Format: `YYYY-MM-DD`. | +| `alg` | Algorithm. Always "RS256" (RSA Signature with SHA-256). | +| `n` | RSA Modulus. The public key modulus, base64url-encoded. | +| `e` | RSA Exponent. The public key exponent, base64url-encoded. Usually "AQAB" (65537). | + +**Caching:** + +The JWKS endpoint includes caching headers to improve performance: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +Cache-Control: public, max-age=3600 +``` + +Clients should cache the JWKS for up to 1 hour (3600 seconds) and only re-fetch when: +- The cache expires +- A JWT contains a `kid` that's not in the cached JWKS +- Key validation fails (may indicate key rotation) + +**Verifying JWTs with JWKS:** + +Most JWT libraries support automatic JWKS fetching. Example with `jsonwebtoken` (Node.js): + +```javascript +import jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; + +const client = jwksClient({ + jwksUri: 'https://id.example.com/.well-known/jwks.json', + cache: true, + cacheMaxAge: 3600000 // 1 hour +}); + +function getKey(header, callback) { + client.getSigningKey(header.kid, (err, key) => { + const signingKey = key.getPublicKey(); + callback(null, signingKey); + }); +} + +// Verify a JWT +jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => { + if (err) { + console.error('Invalid token:', err); + } else { + console.log('Valid token:', decoded); + } +}); +``` + +**Key rotation:** + +When Uitsmijter rotates signing keys: + +1. A new key is generated with a new `kid` (based on the current date) +2. The new key becomes the active signing key for new JWTs +3. Old keys remain in the JWKS for a grace period (e.g., 7-30 days) +4. Clients can verify both old and new JWTs during the rotation period +5. Expired keys are eventually removed from the JWKS + +This ensures zero-downtime key rotation without invalidating existing JWTs. + +**Algorithm selection:** + +Uitsmijter supports two JWT signing algorithms: + +- **HS256** (HMAC with SHA-256): Symmetric algorithm using a shared secret. Default for backward compatibility. JWKS not used. +- **RS256** (RSA with SHA-256): Asymmetric algorithm using RSA key pairs. **Recommended for production.** Requires JWKS. + +To enable RS256 and JWKS, set the environment variable: + +```yaml +JWT_ALGORITHM: RS256 +``` + +When `JWT_ALGORITHM=HS256` (or not set), the JWKS endpoint still returns a valid (though empty or legacy) response for compatibility, but HS256 tokens don't require JWKS for verification. + +> **Security recommendation**: Use RS256 in production. It provides better security because: +> - Public keys can be distributed safely (via JWKS) +> - Private keys never leave the authorization server +> - Resource servers can verify tokens without sharing secrets +> - Key rotation is safer and more manageable + +> **Note**: The JWKS endpoint is publicly accessible and does not require authentication. Public keys are safe to distribute—they can only verify signatures, not create them. + ## Profile endpoints ### /token/info (UserInfo Endpoint) @@ -348,6 +581,8 @@ well as about the overall system status over time. | `uitsmijter_authorize_attempts` | Histogram of OAuth authorization attempts regardless of result (success/failure). | | `uitsmijter_oauth_success` | Counter of successful OAuth token authorizations (all grant types). | | `uitsmijter_oauth_failure` | Counter of failed OAuth token authorizations (all grant types). | +| `uitsmijter_revoke_success` | Counter of successful token revocations. | +| `uitsmijter_revoke_failure` | Counter of failed token revocations (authentication failures). | | `uitsmijter_token_stored` | Histogram of valid refresh tokens over time. | | `uitsmijter_tenants_count` | Gauge of the current number of managed tenants. | | `uitsmijter_clients_count` | Gauge containing the current number of managed clients for all tenants. |