Skip to main content
By default, Pylon sessions are opaque 256-bit tokens that the server resolves and can revoke immediately. Most apps should use them. You sometimes need a stateless token format. Two cases: to authenticate microservices that verify without a session-store round-trip, or to pass through systems that expect a JWT. For these, Pylon creates HS256 JWTs from an existing session. The opaque session stays the source of truth; revoke, refresh, and list still use it. The JWT is a short-lived projection that downstream services validate on their own.

When to use JWTs (and when not to)

You cannot revoke a JWT before it expires. So the default lifetime is short (1 hour), and the opaque session is still the revoke target. If a JWT leaks, you must wait for it to expire or rotate PYLON_JWT_SECRET (which invalidates every JWT).

Algorithm

  • HS256 (HMAC-SHA256). Symmetric — PYLON_JWT_SECRET is both signer and verifier.
  • No RS256, EdDSA, or asymmetric mode in mint. Symmetric is correct when one Pylon binary creates and verifies the token. If multiple services must verify, share the secret over your existing secrets channel.
  • Standard JWT envelope: <base64url-header>.<base64url-payload>.<base64url-signature>.

Endpoint

Response:
Errors: The JWT carries the caller’s identity at mint time: sub (user id), tenant_id, and roles. A select-org after minting does not 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 in Authorization: Bearer. Pylon’s token resolver tries in this order:
  1. Admin token (constant-time compare)
  2. pk.* API key
  3. JWT (when PYLON_JWT_SECRET is set AND the token’s 3-segment shape looks like a JWT)
  4. Session token
JWT verification runs on every request that carries a JWT-shaped bearer. It checks the signature, alg=HS256, exp not in the past, and 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 signed with the same HS256 secret would verify, whatever its issuer. If you share PYLON_JWT_SECRET with another system (microservices, a third-party-token setup), a token issued for that system’s sub could sign into Pylon as that user. Set PYLON_JWT_ISSUER and require the iss claim to match to prevent this. If PYLON_JWT_SECRET is set but PYLON_JWT_ISSUER is missing, Pylon refuses to verify any JWT (JWT_MISCONFIGURED). This is the safest default while the operator fixes the config.

Accepting JWTs minted elsewhere

/api/auth/jwt is for issuing tokens. To accept JWTs from another system (for example your Auth0 tenant or a homegrown SSO), use the jwt plugin instead:
That plugin validates inbound JWTs against the upstream issuer’s JWKS (RS256 / ES256). It does not issue any tokens of its own. See plugins/integrations.

Security guarantees

  • HS256 + 256-bit secretopenssl rand -hex 32 produces a 32-byte secret. Anything shorter and PYLON_JWT_SECRET validates but the security argument weakens.
  • alg=HS256 enforced — the alg=none attack and HS256/RS256 algorithm-confusion attack don’t apply; Pylon’s verify rejects any header where alg != "HS256".
  • Issuer pin — refuse-to-validate when PYLON_JWT_SECRET is set but PYLON_JWT_ISSUER is missing.
  • No revoke path — JWTs are valid until exp. Keep lifetimes short (1 hour default). The underlying opaque session is still revocable via /api/auth/session / /sessions DELETE.
  • Claims captured at mint timetenant_id and roles snapshot the session at issuance. Updating roles or running /select-org doesn’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