CRUMB a card from devarno-cloud

Chronicle JWT Claims & Dev Tokens

chronicle intermediate 4 min read

ELI5

A JWT is a tamper-evident wax-sealed envelope: the relay only opens it if the wax (HS256 signature) matches the secret, the issuer’s stamp is right, and the expiry hasn’t passed. In dev you can also slip in a hand-written note shaped dev-alice — but only if the door explicitly allows hand-written notes.

Technical Deep Dive

Algorithm and Configuration

Tokens are HS256 JWTs verified with jsonwebtoken::decode. Configuration is environment-driven (ADR-002):

Env varPurpose
RELAY_JWT_SECRETHS256 shared secret (required for production verification)
RELAY_JWT_ISSUEROptional; if set, iss claim must match
RELAY_JWT_AUDIENCEOptional; if set, aud claim must match
RELAY_ALLOW_DEV_TOKENSWhen true, accepts dev-{user_id} strings as valid identities

ChronicleTokenClaims

classDiagram
class ChronicleTokenClaims {
+String sub
+Option~i64~ iat
+Option~i64~ exp
+Option~i64~ nbf
+Option~String~ iss
+Option~String~ aud
+Option~String~ jti
+Option~String~ name
+Option~String~ email
+Option~String~ color
+Option~String~ permission
+Option~String~ session_id
+display_name() str
}
class VerifiedIdentity {
+String user_id
+String user_name
+Option~String~ color
+Option~String~ session_id
+bool is_dev_token
}
ChronicleTokenClaims --> VerifiedIdentity : "verify()"

display_name() returns name if present, otherwise falls back to sub (relay/src/services/auth.rs:108).

Verification Flow

flowchart TD
A[token string] --> B{starts with 'dev-'?}
B -- yes --> C{allow_dev_tokens?}
C -- no --> R1[DevTokensDisabled → TOKEN_INVALID]
C -- yes --> D[VerifiedIdentity is_dev_token=true]
B -- no --> E{jwt_secret set?}
E -- no --> R2[SecretNotConfigured]
E -- yes --> F[decode HS256 with Validation: exp,nbf,iss,aud]
F -- err: ExpiredSignature --> R3[TokenExpired]
F -- err: InvalidSignature --> R4[SignatureInvalid]
F -- err: InvalidIssuer --> R5[IssuerMismatch]
F -- err: InvalidAudience --> R6[AudienceMismatch]
F -- ok --> G[ChronicleTokenClaims → VerifiedIdentity]

Dual Strategy

TokenFormatWhen
JWTheader.payload.signature (HS256)Production
Devdev-{user_id} literalDev only, gated by RELAY_ALLOW_DEV_TOKENS=true

Dev tokens carry no name, email, or color; the user_name therefore equals the user_id until the client provides a separate display name.

Key Terms

  • AuthError → enum in services/auth.rs covering every reason a verification can fail; mapped to ErrorCode::Token* for wire transport.
  • permission claim → optional string "viewer" | "editor" | "admin"; the relay parses it but the codebase currently uses a separate PermissionLevel enum that is #[allow(dead_code)] — see services/connection.rs.
  • is_dev_token → audit-only flag preserved on VerifiedIdentity so logs can flag dev sessions.

Q&A

Q: What happens if RELAY_JWT_SECRET is unset and a JWT arrives? A: AuthError::SecretNotConfiguredTOKEN_VERIFICATION_FAILED on the wire.

Q: Is iss always validated? A: Only if RELAY_JWT_ISSUER is set; same for RELAY_JWT_AUDIENCE. They are opt-in tightenings.

Q: Can a dev token impersonate any user? A: Yes — that is the design. dev-alice becomes user_id alice with no signature check, which is why RELAY_ALLOW_DEV_TOKENS MUST be false in production.

Examples

A successful production exchange: client sends {"type":"authenticate","token":"eyJ...","protocol_version":1}; relay decodes claims sub="u_42", name="Ada", color="#D4A574"; relay replies auth_result { success: true, user_id:"u_42", user_name:"Ada" } and stores a VerifiedIdentity on the session.

neighbors on the map