When to use JWTs (and when not to)
| Want | Use |
|---|---|
| Browser session, native app sign-in | Opaque session (default) — revocable, no key to rotate |
| Service-to-service that can’t pay the session-store round-trip | HS256 JWT |
| Accept tokens minted elsewhere (e.g. Auth0, third-party SSO) | Use the jwt plugin instead of /api/auth/jwt |
| Need to revoke a specific token before it expires | Opaque session, not JWT |
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
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/auth/jwt | POST | authenticated session | Exchange the current session for a JWT |
| Status | Code | Reason |
|---|---|---|
| 401 | AUTH_REQUIRED | Caller isn’t signed in |
| 501 | JWT_NOT_CONFIGURED | PYLON_JWT_SECRET is not set or empty |
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:
| Code | Reason |
|---|---|
INVALID_JWT | Signature mismatch, expired, wrong issuer, malformed |
JWT_MISCONFIGURED | PYLON_JWT_SECRET is set but PYLON_JWT_ISSUER is missing — refuse-to-validate by design (see Security guarantees) |
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