Auth Service & Providers
A merod node’s HTTP server (admin API, JSON-RPC, WebSocket, SSE) can be
protected either by an external proxy or by a bundled authentication service,
mero-auth. This page describes the two modes, the JWT model the auth service
uses, the providers it ships, and the local-development mock-token endpoint. For
where auth fits into hardening a deployment, see
Hardening & Security; for the auth_mode config key and
init flags, see the config reference and
merod reference.
Embedded vs proxy auth
Section titled “Embedded vs proxy auth”The server.auth_mode setting picks how requests are authenticated.
The node does not authenticate requests itself. It expects an external, authenticating proxy in front of it that decides who gets through. This is the default. With no proxy in place, the server is effectively unauthenticated, so the loopback bind (see Hardening) matters.
The node builds the mero-auth service in-process and serves its routes
alongside the node’s own endpoints. The node issues and validates JWTs itself —
no external auth proxy is required for authentication (you still terminate TLS at
the edge). Enable it with merod init --auth-mode embedded, by setting
server.auth_mode = "embedded", or per run with merod run --auth-mode embedded.
In embedded mode the auth store path (auth_storage_path, default auth) is
resolved relative to the node home, so the auth database lives under
<home>/<node>/auth.
The JWT model
Section titled “The JWT model”The auth service issues JSON Web Tokens. The relevant details, grounded in the token manager:
- Algorithm: HS256 (symmetric). Tokens are signed with a secret that the service generates and stores in its own storage backend — there is no external key file to manage.
- Two token types: a short-lived access token and a longer-lived refresh token.
- Default lifetimes: access token
3600seconds (1 hour), refresh token 30 days. Configurable viajwt.access_token_expiry/jwt.refresh_token_expiry. - Issuer:
jwt.issuer(the shipped example config usescalimero-auth).
Access-token claims:
| Claim | Meaning |
|---|---|
sub | Subject — the key ID the token was issued for. |
iss | Issuer (jwt.issuer). |
aud | Audience — the client ID. |
exp / iat | Expiry and issued-at, as Unix timestamps. |
jti | Unique token ID. |
permissions | The permission strings granted to the token. |
node_url | Optional node URL the token is scoped to. |
Permission scopes
Section titled “Permission scopes”The permissions claim is the part of the token that actually gates API access.
It is a list of permission strings; the guard parses each one back into a
structured Permission and checks it against what the requested route needs. A
permission has three parts:
- A category — one of
admin,application,package,blob,context, orkeys— usually followed by an action (and sometimes a sub-action), e.g.application:install,blob:add:stream,keys:create. - A resource scope in
[...]— either omitted/*for all resources or a specific id, e.g.context:execute[<ctx-id>]limits the grant to one context. - A user scope for the permissions that carry one (
context:execute,context:invite,context:leave) —*for any identity or a specific identity, encoded as the second field inside the brackets.
The brackets are positional: category:action[<resource>,<user>,<method>].
context:execute[<ctx>,<identity>,<method>] is the fullest form — execute on
one context, as one identity, restricted to one method — and any trailing field
may be left empty to widen the scope.
| Example string | Grants |
|---|---|
admin | Everything. The master permission satisfies every other check. |
application:install | Install applications (any application). |
application:list[<app-id>] | List, scoped to one application id. |
blob:add:stream | Add blobs via the streaming upload path only. |
keys:create | Mint client keys. |
context | All context operations (any context). |
context:execute[<ctx>,<identity>,set] | Call only the set method, on one context, as one identity. |
context:capabilities:grant / context:capabilities:revoke | Manage the per-context capability grants on the auth side. |
An admin token carries a single string:
{ "permissions": ["admin"] }A scoped frontend token grants only what one app’s UI needs — read its context, call a couple of methods, and add blobs:
{ "permissions": [ "context:list[<ctx-id>]", "context:execute[<ctx-id>,<identity>,set]", "context:execute[<ctx-id>,<identity>,get]", "blob:add:stream" ]}Secret rotation
Section titled “Secret rotation”The signing secrets are generated by the auth service itself and rotated on a schedule — there is no key file for an operator to provision or rotate by hand. Three independent secrets exist: the JWT auth secret (signs access and refresh tokens), the JWT challenge secret (signs login challenges), and a CSRF secret.
- Cadence. A background task wakes every hour and rotates any secret that has reached the rotation interval — 24 hours by default. Each rotation generates a fresh 32-byte random value as the new primary.
- Storage layout. Each secret is written under a primary storage key and mirrored to a backup key. The backup is a durability fallback: if the primary cannot be read, the service restores it from the backup. Each stored secret also carries a 48-hour expiry that ages out the retained prior copy in memory.
- Validation uses the current secret. Tokens are verified against the current primary signing secret only. A rotation therefore caps the practical lifetime of already-issued tokens at the rotation interval — clients re-authenticate through the normal login flow once their tokens stop validating.
Challenge tokens and key rotation
Section titled “Challenge tokens and key rotation”Two more time bounds matter when integrating a login flow:
- Challenge lifetime. A login challenge issued by the auth service is deliberately short-lived: it expires 5 minutes (300 s) after issue, so the client must complete the challenge-response within that window.
- Client-key rotation on refresh. When a client token is refreshed, the auth service rotates the underlying key id — it derives a new client id, stores the new key, then deletes the old one. The previous client id no longer exists after a successful refresh, so always keep the newest token pair you receive. Root tokens are the exception: a refresh keeps the same key id.
node_url binding
Section titled “node_url binding”A token may carry an optional node_url claim scoping it to a specific node.
When that claim is present, the auth service validates it against the request’s
Host header (or X-Forwarded-Host when a proxy sets it): if the host the token
was minted for does not match the host the request is hitting, validation fails.
This matters behind a shared reverse proxy fronting several nodes — a token
minted for node-a.example.com will not validate against node-b.example.com,
even though both sit behind the same proxy. Requests coming from the internal
auth service (host exactly auth, or auth:<port> with a numeric port) skip the
check; because the port must be all digits, forged hosts such as
auth:evil.example.com cannot use it to bypass binding.
Providers
Section titled “Providers”The auth service loads providers from a registry and enables them per the
[providers] map in its configuration. Today the registry ships a single auth
provider:
| Provider | ID | Enable with | Status |
|---|---|---|---|
| Username / password | user_password | [providers] user_password = true | Available, enabled in the shipped example config. |
The user_password provider validates a { username, password } credential and
has tunable bounds via [user_password] (min_password_length default 8,
max_password_length default 128).
Mock-token endpoint
Section titled “Mock-token endpoint”For CI and local development, the auth service can expose a mock-token
endpoint at POST /auth/mock-token that mints valid JWTs without any
authentication flow. It is part of the [development] config and is disabled
by default.
| Option | Default | Purpose |
|---|---|---|
enable_mock_auth | false | Enable the mock endpoint. Returns 404 when off. |
mock_auth_require_header | true | Require an Authorization header on the mock endpoint. |
mock_auth_header_value | unset | Specific header value the endpoint requires (e.g. Bearer test-auth-token). |
A minimal dev configuration:
[development]enable_mock_auth = truemock_auth_require_header = truemock_auth_header_value = "Bearer test-auth-token"Requesting a token:
curl -X POST http://localhost:3001/auth/mock-token \ -H "Content-Type: application/json" \ -H "Authorization: Bearer test-auth-token" \ -d '{ "client_name": "ci-test-client", "permissions": ["admin"] }'The response carries access_token and refresh_token, plus warning headers
(X-Mock-Token: true, X-Warning: Mock token - for testing only) and a key ID
prefixed with mock_ so mock tokens are traceable. The tokens are real,
fully-valid JWTs that pass all normal validation — which is exactly why the
endpoint must never be enabled in production.
Getting an embedded-auth node ready
Section titled “Getting an embedded-auth node ready”In embedded mode the auth service mints a JWT for an authenticated client, and the node’s admin API validates that bearer token in-process on every protected call:
-
Initialize (or reconfigure) the node for embedded auth:
Terminal window merod --node node1 init --auth-mode embedded -
Confirm
server.auth_mode = "embedded"inconfig.tomland that an embedded-auth section is present. -
Run the node. The auth store is created under
<home>/node1/auth:Terminal window merod --node node1 run -
Authenticate against the auth endpoints to obtain a JWT, then send it as a bearer token to the admin API. The flow and endpoints are covered in the admin API reference.
Out of scope here
Section titled “Out of scope here”The mero-auth crate carries more surface than node operators normally touch.
The following exist in the crate but are beyond this operate page; consult the
auth service directly if you need them:
- Root-key and client-key management endpoints (creating, listing, and deleting keys, and per-key permission management).
- CORS and security-header configuration (
[cors],[security.headers], CSP) — relevant when serving the auth UI cross-origin. - Challenge / callback routes, which back a challenge-response flow that no shipped provider currently drives.
- Standalone operation:
mero-authcan also run as its own service (default listen127.0.0.1:3001) rather than embedded in the node; that deployment shape is not covered here.