When to use JWTs (and when not to)
JWTs cannot be revoked before they expire. That’s why the default lifetime is short (1 hour) and the opaque session is still the canonical revoke target. If a JWT leaks, you have to wait it out or rotate
PYLON_JWT_SECRET (which invalidates every JWT).
Algorithm
- HS256 (HMAC-SHA256). Symmetric —
PYLON_JWT_SECRETis both signer and verifier. - No RS256 / EdDSA / asymmetric mode in
mint. Symmetric is the right choice when one Pylon binary mints + verifies. If multiple services need to verify, share the secret over your existing secrets channel. - Standard JWT envelope:
<base64url-header>.<base64url-payload>.<base64url-signature>.
Endpoint
The JWT carries the caller’s identity at mint time —
sub (user id), tenant_id, roles. A select-org after minting does not retroactively update the JWT; the next call to /api/auth/jwt reflects the new active tenant.
Claims
Bearer flow
A request can carry either an opaque session token or a JWT inAuthorization: Bearer. Pylon’s token resolver tries in priority order:
- Admin token (constant-time compare)
pk.*API key- JWT (when
PYLON_JWT_SECRETis set AND the token’s 3-segment shape looks like a JWT) - Session token
alg=HS256, exp not in the past, iss matches PYLON_JWT_ISSUER if set.
If verification fails, Pylon returns 401 with one of:
Configuration
Why PYLON_JWT_ISSUER is required
Without an issuer pin, any JWT minted with the same HS256 secret for any issuer would verify. If you share PYLON_JWT_SECRET with another system (microservices, accept-third-party-tokens setup), a token minted for that system’s sub could sign into Pylon as that user. Setting PYLON_JWT_ISSUER and requiring the iss claim to match closes that hole.
If PYLON_JWT_SECRET is set but PYLON_JWT_ISSUER is missing, Pylon refuses to verify any JWT (JWT_MISCONFIGURED) — the safest default while the operator fixes the config. This caught a real audit finding (Wave-5 codex review).
Accepting JWTs minted elsewhere
/api/auth/jwt is for issuing. If you want Pylon to accept JWTs minted by another system (e.g., your Auth0 tenant, a homegrown SSO), use the jwt plugin instead:
Security guarantees
- HS256 + 256-bit secret —
openssl rand -hex 32produces a 32-byte secret. Anything shorter andPYLON_JWT_SECRETvalidates but the security argument weakens. alg=HS256enforced — thealg=noneattack and HS256/RS256 algorithm-confusion attack don’t apply; Pylon’s verify rejects any header wherealg != "HS256".- Issuer pin — refuse-to-validate when
PYLON_JWT_SECRETis set butPYLON_JWT_ISSUERis missing. - No revoke story — JWTs are valid until
exp. Keep lifetimes short (1 hour default). The underlying opaque session is still revocable via/api/auth/session//sessionsDELETE. - Claims captured at mint time —
tenant_idandrolessnapshot the session at issuance. Updating roles or running/select-orgdoesn’t invalidate existing JWTs.
Where to go next
- Sessions — the opaque-token default and why it’s usually the right answer
- API keys — long-lived server-to-server bearer tokens, also stateless but with explicit revoke
- Plugins / integrations — accepting JWTs from upstream IdPs