Security Guide¶
Comprehensive security baseline for Flarelette JWT Kit across HS512 and EdDSA profiles.
Cryptographic Profiles¶
The kit supports exactly two JWT algorithms 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)
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}")
EdDSA Keypairs¶
Generate with CLI:
Output:
{
"kid": "ed25519-2025-01",
"publicJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "<base64url-public-key>",
"kid": "ed25519-2025-01"
},
"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:
- HS512 or EdDSA explicitly enforced (not both in same environment)
- Secrets stored as Cloudflare bindings (
*_NAMEpattern) - TTL ≤ 15 minutes; leeway ≤ 90 seconds
- 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.