Skip to content

Realms & M2M

When a service app is a realm member, the SDK self-discovers the shared scope and transparently substitutes it — your application code does not change. The SDK also adds the no-user m2m primitives for Flow B.

Everything here is non-breaking for standalone services: with no realm, effective_scope == service_name, and a pre-realm Sentinel (no /realm endpoint) degrades gracefully — the SDK stays standalone, it never crashes.

Scope self-discovery

At startup (inside sentinel.lifespan) the SDK calls GET /realm/whoami and caches the result. Two read-only properties expose it:

sentinel.effective_scope   # "acme-suite" for a member, else the service_name
sentinel.realm             # {"slug": "acme-suite", "name": "Acme Suite"} or None

You rarely call these directly — the PermissionClient and RoleClient owned by the Sentinel instance are automatically pointed at effective_scope, and AuthzMiddleware accepts an authz token whose svc is the realm slug (Flow A). To resolve scope manually (e.g. outside the lifespan):

data = await sentinel.fetch_whoami()
# {"service_name": ..., "effective_scope": ..., "realm": {...} | None} or None on a pre-realm Sentinel

Accepting an m2m token (receiver — Flow B)

verify_m2m_token verifies an inbound no-user token and returns a SystemAuth. Trust is rooted in Sentinel's RS256 signature plus aud/type/svc binding — never app-to-app trust.

from sentinel_auth import SystemAuth   # exported from the package root

sys_auth: SystemAuth = sentinel.verify_m2m_token(token)
sys_auth.caller        # the realm member that minted it (server-stamped)
sys_auth.svc           # the realm slug
sys_auth.actions       # ["*"] = full in-realm trust in v1
sys_auth.can("reports:export")   # True if "*" in actions or the action is listed

SystemAuth is the no-user counterpart to RequestAuth — it carries service identity only, never a user. verify_m2m_token raises SentinelError (status_code 401 for a bad/expired/wrong-type token, 403 for the wrong realm or wrong target).

require_system dependency

Gate a system-only route with the require_system FastAPI dependency, which reads the m2m token from Authorization: Bearer:

from fastapi import Depends
from sentinel_auth import SystemAuth

@app.post("/internal/reindex")
async def reindex(sys: SystemAuth = Depends(sentinel.require_system)):
    if not sys.can("search:reindex"):
        raise HTTPException(403)
    ...

Exclude m2m routes from the auth middleware

An m2m call carries no IdP token, so AuthzMiddleware would 401 it. Add the route's path to the middleware exclude_paths and gate it with require_system instead.

Minting an m2m token (sender — Flow B)

mint_m2m_token mints (or returns a cached) token for an outbound system call. It caches the token and only re-mints once it passes ~80% of its TTL, so a tight background loop does not hammer Sentinel. Requires this service to be an active realm member (Sentinel rejects a standalone caller with 403).

token = await sentinel.mint_m2m_token()
async with httpx.AsyncClient() as client:
    await client.post(
        "http://app-b.internal/internal/reindex",
        headers={"Authorization": f"Bearer {token}"},
    )

End-to-end

Side Call Used for
Sender await sentinel.mint_m2m_token() get a token for an outbound system call
Receiver sentinel.verify_m2m_token(token)SystemAuth accept an inbound system call
Receiver (FastAPI) Depends(sentinel.require_system) gate a route to in-realm system callers
Either sentinel.effective_scope / sentinel.realm inspect the discovered scope

For the equivalent JS calls (M2mTokenClient, verifyM2mToken, fetchWhoami) see JS Server Utilities. For the wire format see API → Realms.