JWT Tokens
JWT structure, claims, session binding, and verification in Atom.
Atom issues JWTs signed with ES256 (ECDSA P-256 + SHA-256). Tokens are short-lived (default 1 hour) and tied to a server-side session that can be revoked independently.
Claims
| Claim | Type | Description |
|---|---|---|
sub | string (UUID) | The entity that authenticated |
sid | string (UUID) | The session row ID — used for revocation checks |
tid | string (UUID) or null | The entity's tenant, if any |
iat | number | Issued-at timestamp (Unix seconds) |
exp | number | Expiry timestamp (Unix seconds) |
The JWT header also carries a kid (Key ID) that identifies which signing key to use for verification:
Session binding
Every JWT references a session row via the sid claim. On every token verification, Atom:
- Decodes the JWT and extracts
sid. - Looks up the session row.
- Checks that
revoked_at IS NULLandexpires_at > now().
This means:
- Logout is instant — revoking the session immediately invalidates all tokens issued for that session, without waiting for JWT expiry.
- Status changes propagate — suspending an entity's session revokes the token; there is no window where a suspended entity holds a valid token beyond the check period.
Expiry
Default TTL is 3600 seconds (1 hour), configurable via JWT_EXPIRY_SECS. For long-running services that need persistent access, use API keys instead.
Refresh tokens
Set ATOM_REFRESH_TOKENS_ENABLED=true (requires ATOM_KEY_ENCRYPTION_KEY) to have login also return a long-lived opaque refresh token — an atom_rt_<id>_<secret> credential, verified the same way as access tokens (a KEK-keyed HMAC digest, never stored or logged in plaintext) but kept in its own table and never accepted by Bearer/API-key auth, gRPC, broker auth, or certificate enrollment.
Send this request without an Authorization header — the refresh token itself is the credential, and the normal request wrapper would otherwise reject an expired JWT before this resolver runs (the same reason refreshSession, below, cannot recover from an already-expired token).
Each exchange consumes the presented token and returns a new access JWT plus a new refresh token for the same session; the previous refresh token stops working immediately. refreshTokenExpiresAt is the family's fixed absolute deadline — it never moves forward on rotation, so a session cannot be kept alive indefinitely by repeated refreshing. Presenting an already-consumed token is treated as token theft: it revokes the session and every token in its family, not just the one presented.
refreshSession (the older, JWT-authenticated session-extension mutation) is deprecated but unchanged, and still requires a currently valid access JWT.
Verification flow (Atom-internal)
- Extract
kidfrom the JWT header. - Look up the corresponding public key in the in-memory key store (
primaryorstandby). - Verify the ES256 signature.
- Validate
exp. - Query the
sessionstable to confirm the session is not revoked or expired.
External verification
If your service holds a copy of Atom's public keys (fetched from GET /.well-known/jwks.json), it can verify the JWT signature locally without calling Atom. This proves:
- The token was issued by Atom.
- The token has not expired.
It does not prove:
- The session has not been revoked.
- The entity is still active.
For full authorization (including policy evaluation), always call POST /authz/check. External JWT verification is appropriate only for identity checks where revocation latency is acceptable.
See JWKS & Key Rotation for details on external verification setup.