HelloJohn / docs
Security

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

PropertyValue
AlgorithmEdDSA (Ed25519)
Key typeAsymmetric (private key signs, public key verifies)
Key size32-byte private key, 32-byte public key
Signature size64 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

TokenDefault TTLConfigurable
Access token1 hourYes (access_token_ttl)
Refresh token30 daysYes (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:

  1. A new Ed25519 key pair is generated
  2. The new key is added to the JWKS endpoint with a new kid
  3. New tokens are signed with the new key
  4. The old key remains in JWKS for 24 hours so existing valid tokens continue to work
  5. 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.json

During 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:

CheckDescription
algMust be EdDSA — reject any other algorithm
issMust be https://api.hellojohn.dev
audMust match your tenant ID
expMust be in the future
iatShould not be too far in the past
SignatureVerified 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:

AttackMitigation
Algorithm confusion (alg: none)Rejected — HelloJohn only issues EdDSA
Algorithm confusion (HS256 with public key)N/A — EdDSA is not vulnerable to this
kid injectionkid values are validated against JWKS; arbitrary files cannot be loaded
Expired token replayexp 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)"

On this page