asobi_ops_token (asobi v0.72.5)

View Source

The minted, env-scoped ops token: how a managed tenant's browser reaches this environment's ops plane.

console.asobi.dev authenticates the tenant, checks it owns this environment, maps its role onto capability classes and mints a token. The browser then talks to this environment directly, because the control plane must not read a tenant game database and a browser can already reach an env pod while the control plane cannot.

The shape

v1.<base64url(payload)>.<base64url(mac)>

payload is JSON: env, sub, caps, iat, exp. The MAC covers v1.<payload> - the version prefix is signed, so a future version cannot be stripped down to this one.

The key

ops_token_secret, from ASOBI_OPS_TOKEN_SECRET - a per-environment secret that signs ops tokens and does nothing else.

It was briefly derived from ENGINE_API_KEY, because both sides already held that value. Deriving through a label stops two keys being confused; it does not stop them being compromised together, and a value that both authenticates this engine to the control plane and signs the operator credentials it accepts is one leak away from doing both for an attacker. So it is its own secret, generated per environment by the provisioner and held nowhere else - not even in the control plane's database.

Being per environment, a token minted for one env cannot validate at another even before env is checked. env is checked anyway.

Rotating it revokes every token outstanding for this environment at once, which is the only revocation there is.

What a signature does not buy

A valid MAC is necessary and not sufficient. exp must be in the future, and the whole lifetime must be short - a token signed with a year-long exp is refused, so a mint bug on the other side of the wire cannot issue one this side will honour. Capability classes outside ADR 0007's vocabulary are refused rather than ignored, and an unknown env is refused.

Nothing here is stateful: there is no revocation list, which is exactly why the lifetime is capped rather than merely checked.

Summary

Functions

Whether this node can verify a minted token at all.

The longest lifetime this node will honour, in seconds.

Mint a token for Claims.

Verify Token against the configured environment, returning its claims.

Types

claims()

-type claims() ::
          #{env := binary(),
            sub := binary(),
            caps := [asobi_ops_caps:class()],
            iat := integer(),
            exp := integer()}.

Functions

configured()

-spec configured() -> boolean().

Whether this node can verify a minted token at all.

Exported so asobi_console_env's enable decision reads the same pair verify/1 does. A managed environment configures no ops_secret, so without this the console would be switched off underneath the one credential it accepts.

max_ttl()

-spec max_ttl() -> pos_integer().

The longest lifetime this node will honour, in seconds.

sign(Key, Claims)

-spec sign(binary(), claims()) -> binary().

Mint a token for Claims.

Here rather than only in asobi_saas so the two sides cannot drift: the minting side is a consumer of this function's format, and the round trip is tested against it.

verify/1

-spec verify(binary()) -> {ok, claims()} | {error, atom()}.

Verify Token against the configured environment, returning its claims.

Every rejection is one atom and the caller answers 403 for all of them, so a holder of a bad token cannot tell which check failed.