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
| Token | Prefix | Default lifetime | Where it lives | What it does |
|---|---|---|---|---|
| Access | sess_… | 15 minutes | Authorization: Bearer on every request | Proves the caller |
| Refresh | refr_… | 30 days | Console storage; POST /auth/refresh only | Mints 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
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /auth/login | Public | Username + password → pair |
POST | /auth/refresh | Public | Rotate the pair |
POST | /auth/logout | Session | Revoke this session |
POST | /auth/logout-all | Session | Revoke every session for the subject |
GET | /auth/sessions | Session | List active sessions |
Worked curl: REST identity.
What you bring (outside the product)
| Need | Required? |
|---|---|
| Postgres (or the laptop in-memory store) | Yes — sessions persist here |
| Identity provider | No. Optional for production SSO |
| Extra session service or Redis | No |
sk_ API tokens | No for people; yes for CI and scripts |
What you get
| Outcome | How |
|---|---|
| A person can use the console without an IdP | Seeded local admin + /auth/login |
| Logout takes effect on the next request | Server lookup, not JWT expiry |
| A leaked access token dies quickly | 15-minute lifetime + rotation |
| A leaked database is not a leaked session | Only hashes persist |
| Reload does not 401 the first paint | The console applies the bearer before the UI renders |
| Parallel 401s do not sign the user out | One shared refresh |
Scripts stay on sk_ tokens | Sessions are for interactive login |