How-to

Sessions and refresh tokens

People who use the console get a first-party session. That is not an API token and not an IdP JWT. Login mints two secrets; the runtime stores only hashes. Logout and revoke take effect on the next request.

You do not need an identity provider for this. Postgres (or the in-memory store on a throwaway laptop stack) is enough. An IdP is optional and recommended in production — it issues JWTs and does not use this pair. Scripts and CI should use a revocable sk_ token instead. See Issue an API token and Connect an identity provider.

The token pair

TokenPrefixDefault lifetimeWhere it livesWhat it does
Accesssess_…15 minutesAuthorization: Bearer on every requestProves the caller
Refreshrefr_…30 daysConsole storage; POST /auth/refresh onlyMints a new pair

Both secrets are random 256-bit values. They are opaque, not JWTs. Validation is a store lookup, so a session can be revoked immediately.

flowchart LR
  subgraph client [Browser / console]
    A["Access sess_ — 15 minutes"]
    R["Refresh refr_ — 30 days"]
  end
  subgraph api [Control-plane API]
    V[Validate hash on every request]
    X["POST /auth/refresh — rotate both"]
  end
  subgraph db [Session store]
    H["SHA-256 hashes only"]
  end
  A -->|"Bearer sess_"| V
  V --> H
  R --> X
  X --> H
  X -->|new pair| client

Login

POST /auth/login checks username and password (constant-time, including for unknown users). On success it creates one session row and returns both secrets once. The console stores the pair and sends sess_… on every later call.

sequenceDiagram
  actor User
  participant Console
  participant API
  participant Store as Session store

  User->>Console: username + password
  Console->>API: POST /auth/login
  API->>API: verify password
  API->>Store: store hashes
  API-->>Console: sess_ + refr_ + expiries
  Console->>API: Authorization Bearer sess_
  API->>Store: lookup access hash
  API-->>Console: 200

Seed the first local administrator with SIRIUS_ADMIN_USERNAME and SIRIUS_ADMIN_PASSWORD. Seeding is once-only for the password; rotating it later is a console or API action, not an env-var change.

Refresh

When the access token is about to expire, or a call returns 401, the console posts the refresh token. Sirius issues a new pair and overwrites both hashes. The old refr_… no longer resolves.

Refresh tokens are single-use. The console shares one in-flight refresh so a page load with several 401s does not look like two clients using the same token.

sequenceDiagram
  participant Console
  participant API
  participant Store as Session store

  Note over Console: access expired or 401
  Console->>API: POST /auth/refresh
  API->>Store: lookup refresh hash
  alt valid and not revoked
    API->>Store: replace both hashes
    API-->>Console: new sess_ + new refr_
    Console->>Console: persist and retry
  else missing, expired, or already rotated
    API-->>Console: 401
    Console->>Console: sign out
  end

If a stolen refresh token is used first, the thief receives the new pair and the legitimate console’s next refresh fails — the user is signed out. If the legitimate client already rotated, the stolen token is simply invalid. A consumed refresh token cannot mint another pair.

How a request is authenticated

sess_… is tried first. Anything else falls through to an IdP JWT, then an sk_ API token, then break-glass.

flowchart TD
  Req[Incoming request] --> Sess{"Bearer starts with sess_?"}
  Sess -->|yes| Hash[Lookup access hash]
  Hash -->|live| OK[Subject, tenant, roles]
  Hash -->|missing or revoked| Deny[401]
  Sess -->|no| Next[IdP JWT, then sk_ token, then break-glass]

Endpoints

MethodPathAuthPurpose
POST/auth/loginPublicUsername + password → pair
POST/auth/refreshPublicRotate the pair
POST/auth/logoutSessionRevoke this session
POST/auth/logout-allSessionRevoke every session for the subject
GET/auth/sessionsSessionList active sessions

Worked curl: REST identity.

What you bring (outside the product)

NeedRequired?
Postgres (or the laptop in-memory store)Yes — sessions persist here
Identity providerNo. Optional for production SSO
Extra session service or RedisNo
sk_ API tokensNo for people; yes for CI and scripts

What you get

OutcomeHow
A person can use the console without an IdPSeeded local admin + /auth/login
Logout takes effect on the next requestServer lookup, not JWT expiry
A leaked access token dies quickly15-minute lifetime + rotation
A leaked database is not a leaked sessionOnly hashes persist
Reload does not 401 the first paintThe console applies the bearer before the UI renders
Parallel 401s do not sign the user outOne shared refresh
Scripts stay on sk_ tokensSessions are for interactive login