provision | issue | |
|---|---|---|
| Secret lives at | an external IdP (Auth0 M2M app) | Temper (Temper is the Authorization Server) |
client_id | you supply the IdP's client id | Temper mints it (tmpr_…) |
| Secret | held by the IdP; Temper never sees it | Temper mints a 256-bit secret, returned once, stored only as a SHA-256 hash |
issuer recorded | auth0-m2m | temper |
| Use when | you already run an IdP and want it to keep minting M2M tokens | you want Temper to be the whole loop — no external IdP |
kb_machine_clients registration row. The difference is only where the secret andRegistering a machine does not admit it. Every mint door births the principal denied, so a
correctly registered machine with correct reach still gets403 SYSTEM_ACCESS_REQUIREDon every
single call untiltemper admin access approve <profile_id>runs. Nothing fails earlier — the
secret works, the token mints, the claims are valid — so this is the step that gets skipped and
then looks like a broken credential. Use theprofile_idfrom theprovision/issueoutput;temper admin access revokereverses it.
The issuecommand prints the plaintext secret exactly once. Temper stores only its hash and
cannot recover it — capture it at mint time or rotate to get a new one.
client_id is prefixed tmpr_ — you can tell the two kinds apart at a glance.rotate-secret is the only way to get a fresh plaintext; there is no "show secret".audience row is where integrators get stuck.| Config value | provision (Auth0 mints) | issue (Temper mints) |
|---|---|---|
token_url | your Auth0 tenant's https://<tenant>.auth0.com/oauth/token | your own Temper instance's /oauth/token — e.g. https://temper.acme.com/oauth/token |
client_id | the Auth0 M2M app's client id | the tmpr_… id printed by issue |
client_secret | the Auth0 app's secret | the one-time secret printed by issue |
audience | required — must equal the API's configured AUTH_AUDIENCE, or the minted token fails audience validation at the resource server | omit it — Temper's AS ignores a request-supplied audience entirely and mints with its server-side AS_AUDIENCE |
token_url row is the whole point of the issue path: Temper is the Authorization Server,/oauth/tokenclient_credentials grant alongsideauthorization_code and refresh_token. A conformant client can discover this rather thanGET /.well-known/oauth-authorization-server lists client_credentials ingrant_types_supported and names the token_endpoint.TEMPER_M2M_TOKEN_URL, TEMPER_M2M_CLIENT_ID,TEMPER_M2M_CLIENT_SECRET, and (external IdP only) TEMPER_M2M_AUDIENCE — the same four names inTemper::Credentials), the TypeScript client (temper-ts's ClientCredentials), andclient_credentials protocol — it is Auth0's. Temper's AS nevertemper-rb, temper-ts, and the steward through it) therefore send audience only whentmpr_) credential — an empty audiencetmpr_ credential at all.issuepresumes Temper is your instance's Authorization Server — the modeAS_ISSUERturns
on (self-hosted instances; the same AS that backs SAML). A temper-minted token is signed by the AS
key and carriesiss = AS_ISSUER. An instance has exactly one issuer, so on an Auth0-fronted
instance a temper-minted token would not validate at all — register those machines withprovisionand let Auth0 mint. See ../auth/jwt-verification.md.
application/x-www-form-urlencoded, per RFC 6749 §4. A JSON body isinvalid_request — Auth0 tolerates JSON as an extension, Temper's AS does not.client_secret_post) or in an HTTP Basic headerclient_secret_basic, which the endpoint prefers when present, per RFC 6749 §2.3.1).audience, and the refusal oftests/contracts/m2m-token-request.json. Three suites assert against that one file today: thetemper-ts's tests/contract.test.ts (the clients emit this shape) and the AS's{ access_token, token_type: "Bearer", expires_in }. expires_in comes fromAS_ACCESS_TTL_SECONDS — default 900 seconds (15 minutes).client_credentials. A client should cache the401, since a token checked at the top of a longsub: "<client_id>@clients",azp: "<client_id>", gty: "client-credentials", and no email. That is not cosmetic — it is whyclient_id up in kb_machine_clients, andis_system_adminOR owner of the team that will own the machine.
--owner-team. It records who owns the machine — not what the machine--owner-team, the machine is teamless, and a--team) and the cognitive maps it can write--cogmap). Reach is always explicit and plural — it is never inferred from --owner-team.| Requested reach | You must hold |
|---|---|
--team <ref>[:role] | owner or maintainer on that team (can_manage), and the role may not be owner |
--cogmap <ref>[:ro] | can_grant on that cognitive map |
member/maintainer/watcher,owner) and grant it write on any map they can already delegate — and nothing beyond that. Acan_grant or can_delete on a map: it cannot re-delegate its own| Command | Who | Notes |
|---|---|---|
provision / issue | admin, or owner of the owning team | mint a credential |
rotate-secret | admin, or owner of the machine's team | roll a temper-issued secret |
revoke | admin, or owner of the machine's team | deny the credential |
list / show | admin sees all; a team owner sees only machines owned by their teams | |
rebind | system admin only | see below |
rebind is admin-only, and it is the one exception on purpose. Every other command merelyrebind is different in kind: it transplants an existing agentclient_id. Thatrebind therefore keeps the system-admin bar, and it refusesprovision, neverrotate-secret, not rebind;rebind is for rotating the external IdP application behind an auth0-m2m machine.rotate-secret mints a new secret and keeps the previous one valid for a grace window (defaultnow() is inside that window — the moment it lapses, the old secret--grace 3600 is fine for atmpr_) credential can be rotated this way — an auth0-m2m machine's secret lives at the IdP, androtate-secret refuses it.revoke denies the credential's authentication on the very next request, on both the HTTP APItemper admin machine provision --client-id <id> --label <l> [--owner-team <ref>] [--team <ref>[:role]]... [--cogmap <ref>[:ro]]...
temper admin machine issue --label <l> [--owner-team <ref>] [--team <ref>[:role]]... [--cogmap <ref>[:ro]]...
temper admin machine rotate-secret <machine-id> [--grace <seconds>]
temper admin machine rebind <from-machine-id> --client-id <new-id> --label <l> [--no-revoke-old] # admin only
temper admin machine revoke <machine-id>
temper admin machine list [--include-revoked]
temper admin machine show <machine-id>--team / --cogmap are repeatable — pass one per team or map. A --team ref may carry aacme-eng:maintainer, default member); a --cogmap ref may carry :ro for--owner-team records the machine's owner, never its reach.rebind's --no-revoke-old leaves the old credential live for an overlap window instead ofissue requires an instance whose AS_AUDIENCE is its AUTH_AUDIENCE.AS_ISSUER) that a temper-issued credential requires.tests/contracts/m2m-token-request.json — pin any new clienttemper-rb (spec/temper/credentials_spec.rb), temper-tstests/contract.test.ts), and the AS itselfpackages/temper-cloud/tests/integration/oauth/client-credentials.test.ts).internal/superpowers/specs/2026-07-10-machine-principal-registration-design.mdclient_credentials issuer (Phase B1): internal/superpowers/specs/2026-07-10-machine-principal-phase-b1-issuer-grant-design.mdinternal/superpowers/specs/2026-07-11-machine-principal-phase-b2-team-owner-registration-design.md