Skip to content

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.

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 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 3600 seconds (1 hour), refresh token 30 days. Configurable via jwt.access_token_expiry / jwt.refresh_token_expiry.
  • Issuer: jwt.issuer (the shipped example config uses calimero-auth).

Access-token claims:

ClaimMeaning
subSubject — the key ID the token was issued for.
issIssuer (jwt.issuer).
audAudience — the client ID.
exp / iatExpiry and issued-at, as Unix timestamps.
jtiUnique token ID.
permissionsThe permission strings granted to the token.
node_urlOptional node URL the token is scoped to.

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, or keys — 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 stringGrants
adminEverything. The master permission satisfies every other check.
application:installInstall applications (any application).
application:list[<app-id>]List, scoped to one application id.
blob:add:streamAdd blobs via the streaming upload path only.
keys:createMint client keys.
contextAll 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:revokeManage 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"
]
}

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.

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.

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.

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:

ProviderIDEnable withStatus
Username / passworduser_password[providers] user_password = trueAvailable, 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).

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.

OptionDefaultPurpose
enable_mock_authfalseEnable the mock endpoint. Returns 404 when off.
mock_auth_require_headertrueRequire an Authorization header on the mock endpoint.
mock_auth_header_valueunsetSpecific header value the endpoint requires (e.g. Bearer test-auth-token).

A minimal dev configuration:

[development]
enable_mock_auth = true
mock_auth_require_header = true
mock_auth_header_value = "Bearer test-auth-token"

Requesting a token:

Terminal window
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.

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:

  1. Initialize (or reconfigure) the node for embedded auth:

    Terminal window
    merod --node node1 init --auth-mode embedded
  2. Confirm server.auth_mode = "embedded" in config.toml and that an embedded-auth section is present.

  3. Run the node. The auth store is created under <home>/node1/auth:

    Terminal window
    merod --node node1 run
  4. 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.

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-auth can also run as its own service (default listen 127.0.0.1:3001) rather than embedded in the node; that deployment shape is not covered here.