5.3 Tokens: JWTs, cookies, and bearer headers

A successful login yields a JWT — an HS512-signed token carrying the principal's id and roles. You present it on later requests one of two ways, and the runtime verifies it on every request.

Two ways to present a token

  • Cookie — login sets rs-auth=<token> (HttpOnly). Browsers send it back automatically; it's the natural lane for a same-site web app.
  • Bearer headerAuthorization: Bearer <token>. The lane for API clients, cross-origin browser apps (5.5), and server-to-server calls.

The token is the same either way; only the transport differs. The body of the login response always includes token, so a client that can't or shouldn't use cookies just reads it from there.

Verification is universal

When auth.jwtSecret is configured, the runtime verifies a presented token on every request to the tenant, using a constant-time signature check. Two consequences:

  • A valid token's roles are available to every mount's access check (5.4), including internal pipeline hops (which propagate the principal).
  • A bad token fails closed. An invalid or expired token returns 401 even on an open mount that would accept an anonymous caller. The runtime won't silently ignore a token you sent — if it's bad, the request is rejected.

The practical rule: on 401 with an expired token, drop the token and retry anonymously (or refresh first), rather than resending the stale one.

Sessions and refresh

Tokens expire after sessionMinutes (default 60). To keep a session alive:

  • POST /auth/refresh with a valid token. Past 50% of the token's lifetime it mints a fresh one; before that it returns the current token unchanged (so refreshing early is harmless and cheap).
  • POST /auth/logout clears the cookie and ends the session client-side.

Lockout

Repeated failed logins (maxAttempts, default 5) lock the account for lockMinutes (default 10). During lockout, login returns 401 with a Retry-After header. Because login failures are uniform 401s regardless of cause, an attacker can't distinguish "wrong password" from "no such user" from "locked."

A note on statelessness

In the current version, lockout and session state are node-local (not yet shared across a cluster). For a single node this is transparent; for a multi-node deployment, be aware lockout counters aren't shared between nodes yet (tracked deviation — see the runtime status notes).

Next: turning roles into actual access rules on your mounts.


Previous: 5.2 Users and roles · Manual home · Next: 5.4 Role specs on mounts →