Security Guide¶
Comprehensive security baseline for Flarelette JWT Kit across HS512, EdDSA, ECDSA, and RSA profiles.
Trust Model: Why This Library Is Secure¶
Flarelette JWT Kit is designed from the ground up to prevent the most common JWT vulnerabilities. This section explains exactly how we mitigate historic attacks.
Protection Against Historic JWT Vulnerabilities¶
1. Algorithm Confusion Attacks (CVE-2015-2951: alg: none)¶
Vulnerability: Attacker sets alg: "none" in token header, library accepts unsigned tokens.
Our Protection:
- Mode determined by server configuration only — Verification mode (HS512 vs EdDSA/RSA) is chosen exclusively from server environment variables, never from the token header
- Strict algorithm whitelists — Each mode has an explicit whitelist of allowed algorithms:
- HS512 mode:
['HS512']only - EdDSA/ECDSA/RSA mode:
['EdDSA', 'ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512']only - No
nonealgorithm support — Thenonealgorithm is never included in any whitelist - Token
algtreated as untrusted input — Thealgheader must match the allowed algorithms for the selected mode. Mismatches are rejected.
Code location: src/verify.ts:145-152 (verification with explicit algorithm whitelist)
2. Algorithm Substitution (CVE-2015-9235: RS256 Public Key as HMAC Secret)¶
Vulnerability: Attacker obtains RSA public key, creates HMAC-signed token, library verifies using public key as HMAC secret.
Our Protection:
- Symmetric and asymmetric keys never shared — HS512 and EdDSA/RSA use completely separate code paths
- Configuration conflict detection — Throws error if both
JWT_SECRET(HS512) andJWT_PUBLIC_JWK/JWT_JWKS_*(asymmetric) are configured - Separate verification strategies — Key resolution uses strategy pattern with no code path allowing symmetric key to be used for asymmetric verification
Code location: src/config.ts:36-51 (mode conflict detection)
// SECURITY: Detect conflicting configuration
if (hasHS512 && hasAsymmetric) {
throw new Error(
'Configuration error: Both HS512 (JWT_SECRET) and asymmetric (JWT_PUBLIC_JWK/JWT_JWKS_*) secrets configured. Choose one to prevent algorithm confusion attacks.'
)
}
3. JWKS Injection Attacks¶
Vulnerability: Token includes jku (JWKS URL) or x5u (X.509 URL) header, attacker points to malicious key server.
Our Protection:
- JWKS URL pinned in server configuration —
JWT_JWKS_URLis set in environment variables, never read from token headers - No
jku/x5uheader support — These headers are completely ignored by the library - Service binding JWKS — For Cloudflare Workers, JWKS is fetched via direct Worker-to-Worker RPC (no external URLs)
Code location: src/verify.ts:102-114 (HTTP JWKS with config-only URL)
4. Key ID (kid) Injection Attacks¶
Vulnerability: Attacker manipulates kid header to perform SQL injection, path traversal, or SSRF.
Our Protection:
kidtreated as pure lookup key — Used only for array/map lookups, never interpolated into SQL, file paths, or URLs- JWKS array searched by equality —
kidcompared using strict equality (===), no string concatenation or interpolation
Code location: src/jwks.ts:219 (kid lookup with strict equality)
5. Weak HS512 Secrets¶
Vulnerability: Short HMAC secrets vulnerable to brute force attacks.
Our Protection:
- 64-byte minimum enforced — HS512 requires exactly 64 bytes (512 bits), matching SHA-512 digest size
- Fail-fast on short secrets — Configuration with secrets < 64 bytes throws explicit error with remediation instructions
- CLI tool for secure generation —
npx flarelette-jwt-secret --len=64generates cryptographically random secrets
Code location: src/config.ts:104-109, src/explicit.ts:139-142
// SECURITY: HS512 requires 64-byte minimum (SHA-512 digest size)
if (buf.length < 64) {
throw new Error(
`JWT secret too short: ${buf.length} bytes, need >= 64 for HS512 (use 'npx flarelette-jwt-secret --len=64')`
)
}
6. Mode Confusion Within Library¶
Vulnerability: Library allows both HS512 and asymmetric configuration simultaneously, creating unpredictable behavior.
Our Protection:
- Single-mode enforcement — Configuration error thrown if both symmetric and asymmetric secrets detected
- Explicit mode detection — Mode determined once at startup based on environment variables
Code location: src/config.ts:47-51
Algorithm Pinning at Key Import¶
When importing JWKs, the expected algorithm is provided explicitly to the jose library:
// Inline JWK import — EdDSA requires explicit algorithm hint; EC/RSA auto-detected
const key =
jwk.kty === 'OKP'
? await importJWK(jwk, 'EdDSA') // OKP keys need explicit algorithm
: await importJWK(jwk) // EC/RSA keys: jose auto-detects from kty+crv
The algorithm whitelist in jwtVerify() provides the primary protection (only whitelisted algorithms accepted). Explicit algorithm specification at import time adds a second layer of defense.
Code location: src/verify.ts:71-73
Fail-Silent Pattern with Observability¶
Pattern: All verification failures return null to callers (never throw exceptions).
Rationale:
- Simplifies error handling in HTTP request handlers
- Prevents information leakage via error messages
- Consistent interface for all failure modes
Observability: While the library returns null for all failures, applications should log verification failures with structured metadata:
const payload = await verify(token)
if (!payload) {
// Log failure with context (but not the token itself)
console.warn({
event: 'jwt_verification_failed',
iss: config.iss, // Expected issuer
aud: config.aud, // Expected audience
// DO NOT log the actual token
})
return new Response('Unauthorized', { status: 401 })
}
Recommendation: Track verification failure rates in metrics/APM for anomaly detection.
Cryptographic Profiles¶
The kit supports four JWT algorithm profiles by design. Each has specific security properties and use cases.
HS512 (Symmetric)¶
| Property | Value |
|---|---|
| Algorithm | HMAC-SHA-512 |
| Key material | 64-byte base64url secret |
| Security level | ~256-bit |
| Key distribution | Shared secret between producer and consumer |
| Use case | Internal trusted services with shared secret |
Security properties:
- Fast signing and verification
- Simple key management (single shared secret)
- No public key distribution needed
- Requires mutual trust between producer and consumer
When to use:
- Both producer and consumer are trusted services
- Services can securely share a secret
- Simplest deployment with no key rotation requirements
EdDSA (Ed25519)¶
| Property | Value |
|---|---|
| Algorithm | Ed25519 digital signature |
| Key material | 32-byte private key + public key (JSON Web Keys) |
| Security level | ~128-bit (quantum-safe path exists) |
| Key distribution | Public key distributed via JWKS or inline |
| Use case | One-way trust, public verification, key rotation |
Security properties:
- Asymmetric: private key signs, public key verifies
- Public key can be distributed safely
- Supports key rotation via
kidheader - Resistant to timing attacks
When to use:
- Gateway signs, multiple services verify
- Key rotation required (multiple active keys)
- Zero-trust architecture with distributed services
- Public verification needed (consumers don't need signing capability)
ECDSA (ES256/ES384/ES512) — TypeScript Only¶
| Property | Value |
|---|---|
| Algorithm | ECDSA with P-256, P-384, or P-521 curves |
| Key material | EC private/public key pair (JSON Web Keys) |
| Security level | 128–256-bit depending on curve |
| Key distribution | Public key distributed via JWKS or inline |
| Use case | Self-hosted OIDC gateway (ES512); external OIDC verification |
Security properties:
- Asymmetric: private key signs, public key verifies
- ES512 (P-521) signing available via TypeScript explicit API (
createES512SignConfig) - Verification supports tokens from OIDC providers using any ECDSA curve
- jose auto-detects EC algorithm from JWK
kty/crvfields at import
When to use:
- Verifying tokens from OIDC providers that use ECDSA (ES256 is common; ES512 is FIPS-preferred)
- Self-hosted OIDC gateway that must sign with P-521 for compliance
- When standards requirements mandate ECDSA over EdDSA
Note: ECDSA signing is TypeScript explicit API only. Environment-driven mode detection (JWT_PRIVATE_JWK*) triggers EdDSA, not ECDSA.
Key Generation¶
HS512 Secrets¶
Requirements:
- Minimum 64 bytes (512 bits)
- Cryptographically random
- Base64url-encoded for safe storage
Generate with CLI:
Output:
Generate programmatically:
TypeScript:
import { generateSecret } from '@chrislyons-dev/flarelette-jwt'
const secret = generateSecret(64)
console.log(`JWT_SECRET=${secret}`)
Python:
from flarelette_jwt import generate_secret
secret = generate_secret(64)
print(f"JWT_SECRET={secret}")
Asymmetric Keypairs (EdDSA and ECDSA)¶
The flarelette-jwt-keygen CLI generates EdDSA (Ed25519) or ECDSA (P-256/P-384/P-521) keypairs. EdDSA is the default.
Flags:
| Flag | Description | Default |
|---|---|---|
--alg=<alg> | Algorithm: EdDSA, ES256, ES384, ES512 | EdDSA |
--kid=<id> | Key ID to embed in JWK | <alg>-<timestamp> |
--dotenv | Output as .env JWT_* variable assignments | JSON object |
Generate EdDSA keypair (default — recommended for new deployments):
# Ed25519: fast, compact signatures, strong security. Preferred for new internal services.
npx flarelette-jwt-keygen --kid=ed25519-2025-01
Generate ES512 keypair (ECDSA P-521 — use when FIPS or ECDSA compatibility is required):
# ES512: FIPS-compliant ECDSA P-521. Use when standards mandate ECDSA over EdDSA.
npx flarelette-jwt-keygen --alg=ES512 --kid=es512-2025-01
Generate as .env assignments for direct use:
npx flarelette-jwt-keygen --alg=EdDSA --dotenv
# Outputs:
# JWT_PUBLIC_JWK='{"kty":"OKP","crv":"Ed25519",...}'
# JWT_PRIVATE_JWK='{"kty":"OKP","crv":"Ed25519","d":"...",...}'
JSON output format:
{
"publicJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "<base64url-public-key>",
"kid": "ed25519-2025-01",
"alg": "EdDSA",
"use": "sig"
},
"privateJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "<base64url-public-key>",
"d": "<base64url-private-key>",
"kid": "ed25519-2025-01"
}
}
Best practice for production:
- Generate keys during deployment CI (ephemeral keys no human ever sees)
- Store private key in secret binding immediately
- Distribute public key via JWKS or environment binding
Secret Storage¶
Never Commit Secrets¶
❌ Never do this:
# wrangler.toml - DON'T COMMIT THIS
[vars]
JWT_SECRET = "actual-secret-value" # ❌ Exposed in version control
✅ Use secret-name indirection:
# wrangler.toml - Safe to commit
[vars]
JWT_SECRET_NAME = "MY_JWT_SECRET" # References binding, not value
JWT_ISS = "https://gateway.example.com"
JWT_AUD = "api.example.com"
Environment Scoping¶
Use different secret bindings for each environment.
# wrangler.dev.toml
[vars]
JWT_SECRET_NAME = "JWT_SECRET_DEV"
# wrangler.staging.toml
[vars]
JWT_SECRET_NAME = "JWT_SECRET_STAGING"
# wrangler.production.toml
[vars]
JWT_SECRET_NAME = "JWT_SECRET_PROD"
Deploy secrets to each environment:
wrangler secret put JWT_SECRET_DEV --env dev
wrangler secret put JWT_SECRET_STAGING --env staging
wrangler secret put JWT_SECRET_PROD --env production
EdDSA Key Distribution¶
Production (Service Binding - Recommended):
- Deploy JWT gateway with JWKS endpoint and public key
- Configure consumer workers with service binding
- Keys fetched via direct Worker-to-Worker RPC (private, low-latency)
Benefits:
- No public HTTP endpoint required
- Lower latency (direct RPC, no DNS/TLS overhead)
- Better security (private Worker communication only)
- Integrated with Cloudflare routing
Development/Offline (Inline JWK):
- Deploy public key directly to consumer environment
- Configure
JWT_PUBLIC_JWK_NAMEpointing to secret binding - Note: Requires redeployment for key rotation, no JWKS support
Optional: Thumbprint Pinning
For additional security, pin trusted key thumbprints:
Only keys matching these thumbprints will be accepted for verification.
Key Rotation¶
HS512 Rotation¶
Process:
- Generate new secret
- Deploy new secret to producer and all consumers
- Start signing with new secret
- Wait for maximum token TTL (default 15 min)
- Remove old secret
Downtime: None (if consumers support both secrets during transition)
Frequency: Rotate at least every 90 days or immediately on suspicion of compromise.
EdDSA Rotation¶
Process:
- Generate new keypair with new
kid - Publish new JWKS including both old and new public keys
- Update producer to sign with new key
- Allow dual verification during TTL window
- After TTL expires, remove old key from JWKS
Example:
Before rotation (JWKS):
During rotation (both keys active):
{
"keys": [
{ "kid": "ed25519-2025-01", "kty": "OKP", ... },
{ "kid": "ed25519-2025-02", "kty": "OKP", ... }
]
}
After rotation (old key removed):
Benefits:
- Zero downtime
- Consumers automatically fetch new keys
- No consumer redeployment needed
- Full audit trail via
kidheader
Token Issuance¶
Automatic Claims¶
These claims are automatically populated:
iss— Token issuer (fromJWT_ISS)aud— Token audience (fromJWT_AUD)iat— Issued at (current timestamp)exp— Expiration (current timestamp + TTL)jti— JWT ID (optional, for replay prevention)
Manual Claims¶
Add custom claims with user identity and authorization:
const token = await sign({
sub: 'user123', // Subject (user ID)
permissions: ['read:data'], // Permission strings
roles: ['user', 'editor'], // Role strings
email: 'user@example.com', // OIDC standard claim
tid: 'tenant-123', // Multi-tenant apps
})
Minimal Claims Principle¶
Only include claims necessary for authorization decisions. Never include:
- Passwords or password hashes
- Credit card numbers or payment information
- Social security numbers or national IDs
- Full medical records
- Large datasets (keep tokens < 8KB)
Why: Tokens are transmitted with every request and logged in various places. Treat them as semi-public.
Token Lifetime¶
Default: 900 seconds (15 minutes)
Recommendation:
- External-facing APIs: 15-60 minutes
- Internal service tokens: 5-15 minutes
- Delegated tokens: 5 minutes
Configure via:
Or override per-token:
Token Validation¶
Automatic Verification¶
When calling verify() or checkAuth(), these checks are performed:
- Signature verification — Cryptographic signature valid for detected algorithm
- Issuer check —
issmatchesJWT_ISS - Audience check —
audmatchesJWT_AUD - Expiration check — Token not expired (
exp> now - leeway) - Not before check — If
nbfpresent, token is valid (nbf< now + leeway)
Clock Skew Tolerance¶
Default leeway: 90 seconds
Accounts for:
- Time sync differences between services
- Network latency
- Clock drift
Configure via:
Or override per-verification:
Security consideration: Keep leeway ≤ 90 seconds to avoid excessive expiry drift.
Algorithm Verification¶
The kit rejects tokens with unexpected alg headers. This prevents algorithm substitution attacks.
Example: If environment detects HS512 mode, EdDSA tokens are rejected (and vice versa).
Replay Prevention (Optional)¶
For APIs requiring replay prevention, store jti in a short-TTL key-value store.
import { checkAuth } from '@chrislyons-dev/flarelette-jwt'
const auth = await checkAuth(token, policy().build())
if (!auth) {
return new Response('Unauthorized', { status: 401 })
}
// Check if token was already used
const jti = auth.payload.jti
if (await kv.get(`used:${jti}`)) {
return new Response('Token already used', { status: 403 })
}
// Mark token as used (expires with token TTL)
await kv.put(`used:${jti}`, 'true', {
expirationTtl: auth.payload.exp - Date.now() / 1000,
})
Transport Security¶
TLS Everywhere¶
Never transmit tokens over plaintext HTTP. Always use HTTPS/TLS for:
- External API requests
- Internal service-to-service communication
- JWKS endpoint (if not using service bindings)
Authorization Header¶
✅ Correct:
❌ Never:
- Query parameters:
?token=<jwt>(logged in access logs, proxy logs, browser history) - Request body:
{"token": "<jwt>"}(unnecessarily verbose) - Cookies: (unless specifically designed for cookie-based auth with CSRF protection)
Logging Practices¶
Never log entire tokens. Log only non-sensitive parts:
✅ Safe to log:
console.log({
jti: payload.jti, // JWT ID
sub: payload.sub, // Subject (user ID)
iss: payload.iss, // Issuer
aud: payload.aud, // Audience
exp: payload.exp, // Expiration
action: 'read:data', // Action performed
})
❌ Never log:
console.log(`Token: ${token}`) // ❌ Full token exposed
console.log(`Bearer ${token}`) // ❌ Full token exposed
Redact in APM and telemetry:
- Configure log redaction rules for
Authorizationheaders - Use allowlists for logged fields (never log entire objects containing tokens)
Time and Clock Skew¶
Time Synchronization¶
Depend on platform time sync:
- Cloudflare Workers: NTP-backed, reliable
- Node.js/Python: Ensure host has NTP configured
Leeway Configuration¶
Keep leeway ≤ 90 seconds to prevent excessive expiry drift while accounting for:
- Network latency (typically < 1 second)
- Clock drift (NTP keeps this minimal)
- Service restart time skew
Balance:
- Too low: Legitimate tokens rejected due to minor clock differences
- Too high: Expired tokens accepted for too long
Dependency Security¶
TypeScript Dependencies¶
joselibrary: Pinned version for cryptographic operations- Review changelogs before upgrading
- Run
npm auditregularly
Python Dependencies¶
- Zero external crypto dependencies — uses WebCrypto API directly
- Stdlib only — reduces supply chain risk
Lockfile Management¶
Commit lockfiles:
package-lock.json(npm)yarn.lock(Yarn)pyproject.toml(Python)
Benefits:
- Reproducible builds
- Security scanning can detect vulnerable versions
- Prevents unexpected dependency changes
Automated Updates¶
Use Dependabot or Renovate for automated dependency updates:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
- package-ecosystem: 'pip'
directory: '/packages/flarelette-jwt-py'
schedule:
interval: 'weekly'
Testing and CI/CD¶
Required Test Coverage¶
Unit tests must cover:
- Signature verification (positive and negative cases)
- Claim validation (
iss,aud,exp,nbfwith leeway) - Authorization logic (permissions, roles, predicates)
- Mode detection (HS512 vs EdDSA based on environment)
- Secret-name indirection (resolution and fallback)
Static Analysis¶
Run these checks in CI:
- ESLint (TypeScript/JavaScript)
- Ruff (Python linting)
- mypy (Python type checking)
- TypeScript compiler (type checking)
Secret Scanning¶
Enable secret scanning to prevent committed secrets:
- Gitleaks (open source)
- GitHub Advanced Security (GitHub)
- GitLab Secret Detection (GitLab)
Example Gitleaks config:
# .gitleaks.toml
[[rules]]
id = "jwt-secrets"
description = "JWT secrets and keys"
regex = '''JWT_(SECRET|PRIVATE_JWK|PUBLIC_JWK|JWKS_URL)\s*=\s*["']?[A-Za-z0-9_\-+/={}:,"\.]{32,}["']?'''
Hardening Checklist¶
Before deploying to production:
- Single algorithm mode enforced: HS512 or asymmetric (EdDSA/ECDSA/RSA) — not both in same environment
- Secrets stored as Cloudflare bindings (
*_NAMEpattern) - TTL ≤ 15 minutes; leeway ≤ 90 seconds
-
JWT_AUDis specific per service (no wildcard audiences) — prevents token reuse between services - No tokens in logs, URLs, or version control
- Minimal claims principle applied (no PII unless necessary)
- Rotation policy documented and tested (both HS512 and EdDSA)
- Thumbprint pinning configured (if using EdDSA with strict requirements)
- CI secret scan enabled
- Dependencies pinned in lockfiles
- Incident response runbook prepared
- TLS everywhere (no plaintext transmission)
- Authorization header used (
Authorization: Bearer) - Test coverage includes security-critical paths
Incident Response¶
On Leak or Compromise¶
Immediate actions:
- Rotate secrets/keys immediately
- Revoke sessions by shortening TTL and reissuing tokens
- Review access logs for suspicious activity
- Notify affected users if PII exposed
Investigation:
- Identify scope of compromise (which secrets, how long exposed)
- Review logs for unusual patterns
- Check for permission escalation attempts
Post-incident:
- Document root cause
- Update security procedures
- Add detection for similar incidents
- Consider additional controls (e.g., replay prevention, stricter TTLs)
Audit Logging¶
Log sufficient context for forensics without logging tokens:
console.log({
timestamp: new Date().toISOString(),
jti: payload.jti, // JWT ID
sub: payload.sub, // Subject
iss: payload.iss, // Issuer
aud: payload.aud, // Audience
iat: payload.iat, // Issued at
exp: payload.exp, // Expiration
actor: payload.act?.sub, // Actor service (if delegated)
action: 'read:sensitive', // Action performed
result: 'success', // Outcome
ip: requestIP, // Client IP (if applicable)
})
Threat Model¶
Threats Mitigated¶
- Token forgery — Cryptographic signature prevents creating valid tokens without secret/private key
- Algorithm substitution — Kit rejects tokens with unexpected
algheaders - Expired token reuse — Expiration checks with leeway prevent use of expired tokens
- Clock skew exploitation — Leeway limited to 90 seconds by default
- Permission escalation — Delegated tokens preserve original permissions, no escalation
- Replay attacks — Optional
jtitracking in KV store
Threats Not Mitigated¶
Token theft:
- If attacker obtains valid token, they can use it until expiration
- Mitigate with: Short TTLs (5-15 min), TLS everywhere, secure storage
Compromised secret/private key:
- Attacker can forge tokens indefinitely
- Mitigate with: Secret rotation, access controls, ephemeral keys, HSM storage
Side-channel attacks:
- Timing attacks on signature verification (EdDSA resistant, HS512 uses constant-time comparisons)
- Mitigate with: Use vetted crypto libraries (
jose, WebCrypto)
Distributed denial of service:
- Signature verification is computationally expensive
- Mitigate with: Rate limiting, WAF rules, valid token caching
References¶
- RFC 7519: JSON Web Token (JWT)
- RFC 7517: JSON Web Key (JWK)
- RFC 8693: OAuth 2.0 Token Exchange
- OWASP JWT Cheat Sheet
- Cloudflare Workers Security
Questions or security concerns? Open a security issue or contact the maintainers directly.