Skip to main content
A principal may have several roles. Agents are scoped to their own runs; every other role sees all runs.

API keys

keys.json
A key needs key or key_sha256 and a valid role, or the whole file is rejected. Hash keys (printf '%s' "$KEY" | shasum -a 256) rather than storing them. Generate keys with at least 128 bits of entropy (openssl rand -hex 24). Keys are compared in constant time across all entries. Rotation without downtime: add the new key under the same name with a not_before, give the old one an expires_at, hand out the new key, and remove the old entry later. The server reloads the file on SIGHUP or when it changes (checked every two seconds); a reload that fails to parse keeps the old keys. disabled: true switches a key off immediately.

Managing keys from the server

When the server runs with REVERSI_KEYS, an admin can manage the file over HTTP or in the console instead of editing it. The server rewrites the file (mode 0600) and applies the change at once. All four need the admin role. In the console, Settings has a Users and API keys table with create (name, role, groups, tenant, days to expiry), disable, enable and remove; a new key is shown once, in the response.

OIDC

Bearer JWTs from your identity provider:
Checked on every request: signature against the issuer’s JWKS, RS256 or ES256 only (never none, never HMAC), a kid in the header, iss, aud containing your audience, exp and nbf (60 s skew). The audience check is what stops a token minted for another service from working here: it is required.
  • JWKS. Found through the issuer’s discovery document (/.well-known/openid-configuration) and cached. The key set is re-read every 10 minutes even for known keys, so a key the issuer withdrew stops verifying, and when an unknown kid appears, at most once every 10 seconds so a flood of bad tokens cannot hammer the issuer.
  • Roles. The role claim may be a string, a list, or a nested path like realm_access.roles. Each value is looked up in the role map; unmapped values are ignored, and a token with no mapped role is rejected.
  • Groups. Every value of the role claim also counts as a group name, so an approval can be routed to an identity provider group (approvers: [finance-approvers]) with no extra mapping.
  • Name. sub, or the claim named by REVERSI_OIDC_ACTOR_CLAIM.
  • Tenant. The verifier can read a tenant from a claim (tenant_claim in the library), but the server has no setting for it yet, so OIDC principals carry no tenant there.
The server also accepts API keys at the same time, which is handy for service accounts.

Console modes

The console is controlled by REVERSI_CONSOLE (or console.mode in reversi.config.yaml). open refuses to start unless HOST is loopback (127.0.0.1, ::1 or localhost), and refuses admin as its role. While running it answers only requests whose Host is a loopback name and whose Origin, if any, is the same host; anything else gets 403. So key management, break-glass, holds and the kill switch are not available in open: use login for those. OIDC browser redirect login (a “Sign in with SSO” button) is not built. In login mode a person pastes an OIDC access token; there is no redirect flow.

Sessions

A login produces a signed cookie (HMAC-SHA-256, HttpOnly, SameSite=Strict, eight hours). For API keys the cookie holds the key’s fingerprint, and every request looks the fingerprint up in the live key ring. Disabling, expiring or removing the key ends the session at once, and changing its roles or groups changes what the session may do on the next click. An OIDC session never outlives its token. Logging out invalidates the cookie. Set REVERSI_SESSION_SECRET so sessions survive a restart, and REVERSI_SECURE_COOKIES=1 behind HTTPS.

Slack approvals: user mapping

Slack buttons approve as a Reversi person. The Slack user id is looked up in a map you provide. reversi-server turns the callback (POST /integrations/slack/actions) on when both REVERSI_SLACK_SIGNING_SECRET and REVERSI_SLACK_USERS are set; REVERSI_SLACK_USERS is the path of a JSON file:
Without the secret the route answers 404 Slack approvals are not enabled; a secret without the map file stops the server at start. In code the same map is createApp({ slack: { signing_secret, users } }). roles defaults to ["approver"]. A mapped user whose roles do not include approver or higher (an auditor, say) cannot decide: they get the same “not set up as an approver” answer as an unmapped user. The request must carry a valid Slack signature and a timestamp within five minutes (otherwise 401). Checked locally with signed requests: an unmapped user and a mapped auditor are refused, a mapped approver decides the request. Not tested against real Slack. See approvals.