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.
| 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 is a symmetric algorithm that uses a shared secret key to both sign and verify JWTs.
- Uitsmijter signs JWTs using a secret key (from
JWT_SECRETenvironment variable) - Resource servers verify JWTs using the same secret key
- The secret must be securely shared between Uitsmijter and all resource servers
HS256 is the default algorithm for backward compatibility:
# .env or deployment config
JWT_ALGORITHM: HS256 # or omit this line entirely (defaults to HS256)
JWT_SECRET: your-secret-key-at-least-256-bits
- 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
- Secret management: The
JWT_SECRETmust 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 is an asymmetric algorithm that uses an RSA key pair: a private key for signing and a public key for verification.
- Uitsmijter generates an RSA key pair (2048-bit)
- Uitsmijter signs JWTs with the private key (kept secret)
- Uitsmijter publishes the public key via the JWKS endpoint (
/.well-known/jwks.json) - Resource servers fetch the public key from JWKS
- Resource servers verify JWTs using the public key (no secrets needed)
Enable RS256 by setting the JWT_ALGORITHM environment variable:
# .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.
- 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
- 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
This migration strategy allows you to switch from HS256 to RS256 without invalidating existing tokens or causing downtime.
What changes:
- JWT signing algorithm changes from HS256 to RS256
- JWT header includes
kidfield 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)
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):
import jwt from 'jsonwebtoken';
const secret = process.env.JWT_SECRET;
// Verify token
jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => {
// ...
});
After (RS256 with JWKS):
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
HS256in thealgorithmsarray 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)
Deploy the updated resource servers that support JWKS. Verify that they can still validate existing HS256 tokens.
Test with a sample HS256 token:
curl -H "Authorization: Bearer YOUR_HS256_TOKEN" https://your-api.example.com/protected
The request should succeed, confirming backward compatibility.
Update Uitsmijter’s configuration to use RS256:
Kubernetes/Helm:
# values.yaml
env:
JWT_ALGORITHM: RS256
Docker Compose:
environment:
- JWT_ALGORITHM=RS256
Direct deployment:
export JWT_ALGORITHM=RS256
Restart Uitsmijter to apply the new configuration:
# Kubernetes
kubectl rollout restart deployment/uitsmijter
# Docker Compose
docker-compose restart uitsmijter
Uitsmijter will:
- Generate a new RSA key pair on startup
- Start signing new JWTs with RS256
- Publish the public key at
/.well-known/jwks.json
Test that new tokens are signed with RS256:
- Obtain a new access token:
# 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
- Decode the JWT header (without verifying):
# Extract and decode the header
echo "YOUR_TOKEN" | cut -d'.' -f1 | base64 -d
Expected output:
{
"alg": "RS256",
"typ": "JWT",
"kid": "2024-11-08"
}
- Verify the token works with your resource servers:
curl -H "Authorization: Bearer YOUR_RS256_TOKEN" https://your-api.example.com/protected
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:
# Check when the last HS256 token will expire
# Default token lifetime is 2 hours
After all HS256 tokens have expired (wait at least TOKEN_EXPIRATION_IN_HOURS × 2), you can remove HS256 support from resource servers:
// 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).
If you encounter issues during migration, you can rollback to HS256:
- Change
JWT_ALGORITHMback toHS256(or remove it) - Restart Uitsmijter
- New tokens will be signed with HS256 again
- 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.
With RS256, you can rotate signing keys without downtime:
- Generate a new key by restarting Uitsmijter
- The new key gets a new
kid(current date:YYYY-MM-DD) - New JWTs are signed with the new key
- Old public keys remain in JWKS for verification
- After grace period, old keys can be removed from JWKS
Uitsmijter doesn’t currently implement automatic key rotation, but you can implement it using:
- Scheduled restarts: Restart Uitsmijter monthly/quarterly (generates new key)
- External key management: Use Kubernetes secrets rotation
- Manual rotation: Generate new key via admin endpoint (future feature)
- Grace period: Keep old keys in JWKS for at least 2× token lifetime
- Monitoring: Monitor JWT verification failures during rotation
- Documentation: Document which
kidis active at any time - Testing: Test rotation in staging before production
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).
Cause: JWT_ALGORITHM is still set to HS256 or not set.
Solution: Verify JWT_ALGORITHM=RS256 is set and restart Uitsmijter.
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
kidfrom the JWT header - Check that clocks are synchronized (NTP)
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
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
Controls the JWT signing algorithm.
Values:
HS256(default): HMAC with SHA-256 (symmetric)RS256: RSA with SHA-256 (asymmetric)
Example:
JWT_ALGORITHM: RS256
(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:
JWT_SECRET: your-secret-key-at-least-32-characters-long
Not used when JWT_ALGORITHM=RS256.
Controls JWT access token expiration time.
Default: 2 (2 hours)
Example:
TOKEN_EXPIRATION_IN_HOURS: 8
Affects:
- Access token lifetime
- Grace period for key rotation (should be 2× this value)