9.2 KiB
Authentication and access
Relata has three ways in, for three different callers, and they do not overlap.
| Caller | Mechanism | Gets |
|---|---|---|
| A person, in a browser | OAuth 2.0 Authorization Code + PKCE against Blackwall | A server-side session and a cookie |
| A person, when the identity provider is broken | Local username and password, disabled by default | The same session and cookie |
A program, against /api/v1 |
A bearer token the person created in the interface | A scoped, non-interactive principal |
Everything a signed-out visitor can reach is a static description of the product. No stored item, entity, relationship, count or workspace name is readable without authenticating.
1. The public site
/, /how-it-works and /api are served to anyone. They are hand-written pages describing
what Relata does and what it deliberately does not do; they read nothing from the database.
/ is the only dual-purpose route: signed in, it is the dashboard; signed out, it is the
landing page. Every other application route requires a session and redirects to /login.
2. Signing in through Blackwall
The endpoint to register
https://<your-relata-host>/auth/oauth/callback
The path is fixed. Register the same path under whatever hosts you serve, including
http://127.0.0.1:8080/auth/oauth/callback for local development.
The flow
GET /auth/oauth/startmints a 32-bytestate, a PKCE verifier and (for OIDC) anonce, stores them in a short-lived,HttpOnly,SameSite=Laxcookie scoped to/auth/oauth, and redirects to the provider's authorization URL withcode_challenge_method=S256.- The person authenticates with Blackwall.
GET /auth/oauth/callbackcomparesstateagainst the cookie in constant time, clears the cookie so the value is single-use, exchanges the code and PKCE verifier for tokens over a timeout-bounded HTTP client, and establishes the identity.- Identity comes from the ID token when the provider is OIDC — signature verified against
cached JWKS, with
iss,aud,exp,iatandnonceall checked — and otherwise from the userinfo endpoint. - The user record is found or created, a new session ID is issued, and the browser lands on the dashboard.
Identity is the subject, never the email
Users are keyed on (oauth_provider, oauth_subject) with a unique index. The sub claim is
the only immutable identifier a provider offers; email addresses get reassigned, and keying on
one means whoever receives a recycled address inherits the previous holder's documents.
The email is stored for display and for the optional domain allowlist. It is never the key.
What a new person gets
A private workspace of their own, created on first sign-in, in which they are admin.
Nobody sees anyone else's documents, entities or relationships — the workspace boundary was
enforced in storage from the first commit, and this reuses it rather than adding a new one.
The consequence is worth stating plainly: relationship discovery never spans users. Two people who upload documents about the same contract get no link between them, because as far as Relata is concerned they are separate corpora. That is the cost of the isolation.
Provisioning is idempotent. Two concurrent first sign-ins race on the unique subject index; the loser re-reads the winner's record rather than creating a second workspace.
Local login
Disabled unless RELATA_LOCAL_LOGIN_ENABLED=true. When disabled, the password form is not
rendered and POST /login returns 404 — the handler is not registered at all, rather than
registered and refusing, so there is no code path to reach.
Enable it to recover from a broken provider configuration. relata createadmin still works
regardless, so the account can be prepared in advance.
A user provisioned through OAuth has no password hash and can never sign in locally, whatever is submitted.
3. API bearer tokens
Creating one
Settings → API tokens. Choose a name, the scopes, and an expiry. The token is shown once and never again; only a SHA-256 hash is stored, so a database read cannot be replayed as a credential and neither can a backup.
rlt_v1_8Kx2mQ… (43 characters of base64url from 32 random bytes)
The first 12 characters are stored in clear as a display prefix, so the interface can list
rlt_v1_8Kx2… and you can tell which token is which without holding the secret.
Using one
curl -H "Authorization: Bearer $RELATA_TOKEN" \
https://relata.example/api/v1/search?q=delivery+schedule
Scopes
| Scope | Permits |
|---|---|
ingest |
Create items, upload files, reprocess, delete |
query |
Search, answer generation, and reading stored items, entities and relationships |
review |
Confirming, rejecting and retyping relationships; confirming entity aliases |
A token carries the scopes it was given and never more than the role of the user who created
it. A viewer's token cannot ingest even if ingest was requested, because the check is the
intersection of the two. Demoting a person immediately narrows every token they hold.
Scopes are checked per route. A request outside its scopes gets 403 with
insufficient_scope and the scope it needed, not a generic denial.
What a token cannot do
- Reach the web interface. Bearer tokens authenticate
/api/v1only. A token in a browser request to/itemsis ignored. - Manage tokens. Token creation, listing and revocation require a session. A leaked token cannot mint more tokens or extend its own life.
- Outlive its expiry or revocation. Both are checked on every request against the stored record; there is no cached grant.
- Change the account. Nothing under
/authaccepts a bearer token.
CSRF
Bearer requests do not carry CSRF tokens and are not asked for them: CSRF exists to stop a
third-party page using ambient credentials, and an Authorization header is not ambient — a
browser never attaches one on its own.
The exemption is bound to how the request authenticated, not to a header being present. A
request holding a session cookie still needs its CSRF token even if it also sends
Authorization, so an attacker cannot disable the check by adding a header.
Rate limiting and audit
Each token has its own rate limit bucket. Creation, use-after-expiry, revocation and every authentication failure are audit events. The token value never appears in a log, an audit record, an error message or a metric label.
last_used_at is recorded at most once a minute per token, so a busy token does not turn every
read into a write.
4. Configuration
See .env.example for the full list with comments. The minimum for Blackwall:
RELATA_OAUTH_ENABLED=true
RELATA_OAUTH_PROVIDER_NAME=Blackwall
RELATA_OAUTH_CLIENT_ID=...
RELATA_OAUTH_CLIENT_SECRET=...
# Either discovery:
RELATA_OAUTH_ISSUER=https://www.blackwall.cx
# ...or the endpoints directly:
RELATA_OAUTH_AUTHORIZE_URL=https://www.blackwall.cx/oauth/authorize
RELATA_OAUTH_TOKEN_URL=https://www.blackwall.cx/oauth/token
RELATA_OAUTH_USERINFO_URL=https://www.blackwall.cx/oauth/userinfo
RELATA_OAUTH_SCOPES=openid profile email
RELATA_OAUTH_ALLOWED_EMAIL_DOMAINS= # optional; empty means any
Startup validates this as a set: OAuth enabled with neither an issuer nor a full endpoint triple is refused, as is a non-HTTPS endpoint outside development, as is disabling both OAuth and local login — which would leave no way in at all.
5. Known limitations
- No refresh tokens. Relata's session is its own; the provider's access token is used once to establish identity and then discarded rather than stored. A revoked Blackwall account keeps its Relata session until that session expires (idle 24h, absolute 7 days by default). Revoke the Relata session too, from Settings, if that matters.
- No group or role mapping yet. Every OAuth user is admin of their own workspace. There is nothing to map a provider group onto, because there is no shared workspace to have roles in. If shared workspaces are added later, claim-based role mapping belongs with them.
- No account linking. A local account and an OAuth account with the same email are two separate users with two separate workspaces. Merging them is deliberate manual work, not something to infer from a matching address.
- Token scopes are coarse. Three scopes, checked per route. There is no per-item or per-collection restriction.
- The provider is trusted for identity. If Blackwall asserts a subject, Relata believes it. That is what delegating authentication means.
- A newly created token is rendered only in the creation response. It is never placed in a redirect URL, browser history, referrer header or access-log query string. Refreshing or navigating away cannot recover it because only its hash is stored.
- Key rotation that reuses a key ID costs one failed request. Relata caches the provider's
signing keys for an hour. If a key is replaced while keeping its
kid, the first token signed by the new key fails signature verification, triggers a single throttled refetch, and succeeds on the retry within the same request. A provider that rotates properly — new key, newkid— never hits this path.