Auth, identity & tokens¶
Every request carries a bearer token that resolves to a principal with a capability
set. This page covers the two public sign-in routes (/auth/margonem, /auth/discord),
the Discord ⇄ Margonem identity link, the whoami/identity lookups, the capability ACL
(grant/revoke), and the named service tokens an operator mints. The identity examples
use the fixture Gracz Stefan (Margonem id 100001, character Eraster) from the
canonical session; the model is spelled out in
roles & permissions.
/auth/margonem and /auth/discord are public (no token). whoami/identity need
identity.read, /capabilities needs capability.read, grant/revoke need
capability.admin, and tokens need token.manage — the admin token carries all of them.
Every example runs against a live daemon — see how the reference is tested.
Routes¶
| Method | Path | Cmdlet | Cap | Write |
|---|---|---|---|---|
| POST | /auth/margonem |
Invoke-NerthusMargonemAuth |
— | — |
| POST | /auth/discord |
— (Discord OIDC sign-in) | — | — |
| POST | /auth/link |
Connect-NerthusIdentity |
identity.link |
✓ |
| GET | /auth/whoami |
Get-NerthusWhoami |
identity.read |
— |
| GET | /identity/{id} |
Get-NerthusIdentity |
identity.read |
— |
| GET | /capabilities |
Get-NerthusCapability (the caller's resolved set) |
capability.read |
— |
| POST | /capabilities/grant |
Grant-NerthusCapability |
capability.admin |
✓ |
| POST | /capabilities/revoke |
Revoke-NerthusCapability |
capability.admin |
✓ |
| POST | /tokens |
New-NerthusToken |
token.manage |
✓ |
| GET | /tokens |
Get-NerthusToken |
token.manage |
— |
| DELETE | /tokens/{id} |
Remove-NerthusToken |
token.manage |
✓ |
Cap is the required capability (— = public, no token); Write (✓) marks routes that pass the write gate; a — in Cmdlet is reached directly, no wrapper. Paths are relative to /v1/api; the cross-cutting contract — middleware, envelopes, status codes — is on the API reference index.
The two /auth/* sign-in routes are public and mint nothing until they resolve a Margonem id to a Gracz; they write only private token records, so they stay operable in read-only mode. Token, identity, and ACL semantics: Permissions.
Sign in with a Margonem payload¶
POST /v1/api/auth/margonem verifies an RSA-signed { user_id, token, ts, signatureBase64 }
payload, resolves the Margonem id to a Gracz, and mints a session token. The signature
cannot be forged in a doc example, so this shows the rejection: an unsigned body is 401
with the failure reason in error.
Response 401:
Sign in with a Discord id_token¶
POST /v1/api/auth/discord verifies a Discord OIDC id_token (RS256), resolves the linked
Margonem id, and mints a session token. A body without id_token is rejected 400 before
any verification.
Response 400:
Link a Discord account to a Margonem id¶
POST /v1/api/auth/link binds a Discord id to a Margonem id (one Discord per id; a re-link
revokes the prior). The link stores ids only — the Gracz name is resolved live, so it never
goes stale on a rename. Here Stefan's 100001 links cleanly.
Response 200:
{ "margonem_id": "100001", "discord_id": "555000111222", "gracz": "Stefan", "linked_via": "margonem", "linked_at": "2026-07-01T12:00:00Z" }
Rejected: link without ids¶
A body missing both ids is 400, nothing written.
Response 400:
Who am I¶
GET /v1/api/auth/whoami reflects the caller's resolved principal: its kind,
margonemId, gracz, and effective capabilities. The admin token resolves to the
loopback machine principal with admin.all.
Response 200:
Rejected: no token¶
whoami needs identity.read, so an anonymous request is 401 — authentication precedes
everything.
Response 401:
Resolve an identity¶
GET /v1/api/identity/{id} resolves a Person by Margonem id (or Gracz name) from the model,
returning the Gracz, the Margonem id, and the role-derived capability bundle.
Response 200 (trimmed):
Rejected: unknown identity¶
An id that matches no Gracz is 404.
Response 404:
The caller's capabilities¶
GET /v1/api/capabilities returns the { count, capabilities } set of the caller's own
principal — the admin token holds just admin.all.
Response 200:
Preview a capability grant¶
POST /v1/api/capabilities/grant stacks one capability onto a Person's effective set (on
top of their role bundles). With ?dryRun=true it returns 200 { wouldGrant, margonemId }
and persists nothing.
Response 200:
Rejected: grant without fields¶
Omitting margonemId or capability is 400.
Response 400:
Preview a capability revoke¶
POST /v1/api/capabilities/revoke removes one capability from a Person's set — the mirror
of grant. ?dryRun=true returns 200 { wouldRevoke, margonemId } and writes nothing.
Response 200:
Mint a named service token¶
POST /v1/api/tokens issues a named token scoped to an explicit capability set (or a
role's bundle). The raw tok_… value is returned once, in this response — it is never
stored (the record is keyed by its SHA-256), so it cannot be recovered later.
Response 201:
List issued tokens¶
GET /v1/api/tokens returns the { count, items } envelope — each item is a token's id,
kind, name, and expiry, never its raw value. The ci-tool token minted above is present.
Response 200 (trimmed):
Revoke a token¶
DELETE /v1/api/tokens/{id} deletes the token record by its id. An id that matches no
record is 404.
Response 404: