JWT Security
How HelloJohn signs, rotates, and revokes JWTs — algorithms, key management, and token security best practices.
JWT Security
HelloJohn issues JSON Web Tokens (JWTs) signed with EdDSA (Ed25519) — a modern elliptic-curve algorithm that is faster and more secure than RS256.
Signing Algorithm
| Property | Value |
|---|---|
| Algorithm | EdDSA (Ed25519) |
| Key type | Asymmetric (private key signs, public key verifies) |
| Key size | 32-byte private key, 32-byte public key |
| Signature size | 64 bytes |
Ed25519 was chosen over RS256/HS256 because:
- Smaller signatures — 64 bytes vs 256+ bytes for RSA-2048
- Faster verification — ~2x faster than RSA verification
- No parameter confusion attacks — unlike RSA, no padding oracle vulnerabilities
- Deterministic — same input always produces the same signature
Token Lifetimes
| Token | Default TTL | Configurable |
|---|---|---|
| Access token | 1 hour | Yes (access_token_ttl) |
| Refresh token | 30 days | Yes (session_duration) |
Short access token lifetimes limit the window of exposure if a token is intercepted. See Session Timeouts for configuration.
Access Token Non-Revocability
Access tokens are stateless — HelloJohn does not check a revocation list on every request. This means:
- A revoked session still has a valid access token until it expires
- The maximum exposure window equals
access_token_ttl
For instant revocation, use API-based token verification instead of local JWT verification:
// Instead of local verification (no revocation check):
const payload = await jose.jwtVerify(token, JWKS);
// Use API verification (checks revocation in real-time):
const res = await fetch("https://api.hellojohn.dev/v1/sessions/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HELLOJOHN_SECRET_KEY}`,
"X-Tenant-ID": process.env.HELLOJOHN_TENANT_ID!,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
});
const { valid } = await res.json();For most applications, local verification with a 15-minute access_token_ttl provides an acceptable balance.
Key Rotation
Rotate signing keys periodically or after a suspected compromise.
Rotate via Dashboard
Go to Developer → API Keys → Signing Keys → Rotate.
Rotate via API
curl -X POST "https://api.hellojohn.dev/v1/admin/signing-keys/rotate" \
-H "Authorization: Bearer sk_live_abc123" \
-H "X-Tenant-ID: tnt_01HABCDEF654321"What happens during rotation:
- A new Ed25519 key pair is generated
- The new key is added to the JWKS endpoint with a new
kid - New tokens are signed with the new key
- The old key remains in JWKS for 24 hours so existing valid tokens continue to work
- After 24 hours, the old key is removed from JWKS
This zero-downtime rotation means no users are signed out during the process.
JWKS Endpoint
GET https://api.hellojohn.dev/.well-known/jwks.jsonDuring rotation, both keys appear:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"use": "sig",
"kid": "key_01HABCDEF_new",
"x": "new-public-key-base64url"
},
{
"kty": "OKP",
"crv": "Ed25519",
"use": "sig",
"kid": "key_01HABCDEF_old",
"x": "old-public-key-base64url"
}
]
}The jose library automatically selects the correct key using the kid header in the JWT.
Token Validation Checklist
When verifying a JWT, your backend must check:
| Check | Description |
|---|---|
alg | Must be EdDSA — reject any other algorithm |
iss | Must be https://api.hellojohn.dev |
aud | Must match your tenant ID |
exp | Must be in the future |
iat | Should not be too far in the past |
| Signature | Verified against the JWKS public key |
import * as jose from "jose";
const JWKS = jose.createRemoteJWKSet(
new URL("https://api.hellojohn.dev/.well-known/jwks.json")
);
async function verifyToken(token: string) {
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: "https://api.hellojohn.dev",
audience: process.env.HELLOJOHN_TENANT_ID,
algorithms: ["EdDSA"], // Explicitly require EdDSA
});
return payload;
}Never accept tokens with alg: none — the jose library rejects these by default.
JWT Confusion Attacks
HelloJohn mitigates common JWT attacks:
| Attack | Mitigation |
|---|---|
Algorithm confusion (alg: none) | Rejected — HelloJohn only issues EdDSA |
| Algorithm confusion (HS256 with public key) | N/A — EdDSA is not vulnerable to this |
kid injection | kid values are validated against JWKS; arbitrary files cannot be loaded |
| Expired token replay | exp claim enforced on every verification |
Self-Hosted Key Management
For self-hosted HelloJohn, signing keys are generated from the HELLOJOHN_JWT_SIGNING_KEY environment variable.
# Generate a new Ed25519 private key
openssl genpkey -algorithm ed25519 -out signing_key.pem
# Store in secrets manager (never in Git or plain .env)
aws secretsmanager put-secret-value \
--secret-id hellojohn/jwt-signing-key \
--secret-string "$(cat signing_key.pem)"