Skip to main content

Per-User Token Provisioning

Fleet deployments historically authenticated every developer's plugin with a single shared tenant credential (org_id:license). That gives attribution, not authorization: anyone holding the shared credential can assert any identity. Per-user tokens give every developer a validated, non-forgeable {identity, role} that AxonFlow's fleet plane uses for role-scoped access — so a developer reads only their own audit, decision, and override rows while an admin reads the whole tenant trail.

This page is the provisioning runbook. For the read-scoping model those tokens unlock — the role→scope rule, the enforcement layers, and the v9.10.0 behavior change — see Per-Developer Identity → Role-scoped reads.

The two paths

Two provisioning paths converge on the same validated identity. Pick by whether you run an identity provider — you do not need one to start.

Path A — AxonFlow-managedPath B — IdP-issued (OIDC)
Token issuerAxonFlow admin API (HS256)Your IdP: JumpCloud, Okta, Azure AD… (RS256/JWKS)
Role sourceAdmin-assigned at mintSCIM group→role mappings (never the token's own role claim)
RevocationServer-side deny-list, immediateIdP token lifetime + IdP-side deactivation
Best forTeams without an IdP; evals; break-glassIdP-managed fleets

Path A is not a stopgap — it is the permanent no-IdP tier, and the fastest way to get role-scoped reads for a fleet today.

Version availability

Path A is available on platform v9.10.0 and later. Path B (OIDC) requires platform v9.11.0 or later — it depends on the fleet-mappable system roles and the self-hosted (in-vpc) OIDC key fix described in its prerequisites below. On v9.10.0, use Path A.


Path A — Admin-minted per-user tokens

Prerequisites

  • The customer-portal carries JWT_SECRET (the same value as the agent — minted tokens are validated agent-side) and ADMIN_API_KEY.
  • Platform v9.10.0 or later (the revocation deny-list and role-scoped reads).
The mint credential is a top-tier secret

The role travels as a JWT claim, so whoever can mint sets the role. Mint, rotate, and revoke therefore require a valid X-Admin-API-Key — even in deployment modes where admin auth is otherwise optional (in-vpc). If ADMIN_API_KEY is not configured, the endpoints refuse with 401/503; they never fall open.

ADMIN_API_KEY is the same credential that gates org and license management, and it works across every org — it is now also a "mint an admin-role token for any org" credential. Rotate it on a schedule, restrict it to operators, and never embed it in a fleet-distributed config.

Mint

curl -X POST "$PORTAL_URL/api/v1/admin/organizations/{org_id}/user-tokens" \
-H "X-Admin-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_email": "[email protected]", "role": "developer", "ttl_hours": 720}'

Response (201) — the token is returned exactly once and is never stored or logged:

{
"success": true,
"user_token": {
"token": "eyJ...",
"jti": "f3b0…",
"user_email": "[email protected]",
"role": "developer",
"org_id": "yourco",
"expires_at": "2026-08-15T00:00:00Z"
}
}
  • role must be one of admin, owner, policy_admin, developer, member, viewer.
  • ttl_hours defaults to 720 (30 days), capped at 8760 (1 year). Every token carries exp — unbounded tokens cannot be minted.
  • Emails are canonicalized (lower-cased, trimmed) at mint, validation, and revocation, so audit attribution and read-scoping key on the same value.

Rotate

curl -X POST "$PORTAL_URL/api/v1/admin/organizations/{org_id}/user-tokens/rotate" \
-H "X-Admin-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_email": "[email protected]", "role": "developer"}'

Rotation revokes every existing token for the user (issued before the rotation instant), then mints the replacement. Revocation granularity is one second — a token minted in the same second as the rotation survives it.

Revoke

# One token (by jti — from the mint response or the MINT_USER_TOKEN audit row):
curl -X DELETE "$PORTAL_URL/api/v1/admin/organizations/{org_id}/user-tokens" \
-H "X-Admin-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jti": "f3b0…", "reason": "laptop stolen"}'

# All of a user's tokens (offboarding):
curl -X DELETE "$PORTAL_URL/api/v1/admin/organizations/{org_id}/user-tokens" \
-H "X-Admin-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_email": "[email protected]", "reason": "offboarded"}'

Revocation is enforced server-side on every validation: the deny-list is consulted before a token is accepted, and if the deny-list is unreachable the token is rejected — fail closed. Every mint, rotate, and revoke writes an admin audit row (actor, target user, role, ttl, jti); the token value itself never lands in any log or audit row.

Fleet-scale delivery

Deliver each developer's token through your MDM's managed-settings file so it never transits chat or email. For the Claude Code plugin:

// /Library/Application Support/ClaudeCode/managed-settings.json (per device)
{
"env": {
"AXONFLOW_USER_TOKEN": "<that developer's token>"
}
}

Scope the payload per device/user in your MDM — a fleet-wide static file would hand every developer the same identity (and one developer's token to all others). The same AXONFLOW_USER_TOKEN value drives every integration's X-User-Token header; see Configuring your client for the per-client field and minimum version. Rotate on your credential cadence with the rotate endpoint above.


Path B — IdP-issued OIDC tokens (JumpCloud example)

Your IdP issues each developer a standard OIDC token; AxonFlow validates it against the IdP's JWKS (issuer, audience, expiry, RS256 signature) and resolves the developer's role from the SCIM-synced directory — deliberately not from any role claim inside the token, so role assignment stays on AxonFlow's audited SCIM/admin surface and an IdP misconfiguration cannot mint an admin.

The examples use JumpCloud; Okta, Azure AD (Entra ID), and OneLogin work the same way — substitute that IdP's issuer, JWKS URI, and SCIM steps (Okta · Azure AD · OneLogin).

Prerequisites

  • Platform v9.11.0 or later. Path B depends on the fleet-mappable system roles being seeded per org and, for self-hosted (in-vpc) deployments, on the OIDC config being keyed to your licensed org — both of which land in v9.11.0. On v9.10.0, use Path A.
  • Enterprise deployment. Per-user token resolution runs only for enterprise-authenticated fleet callers. Community and community-SaaS callers keep their existing bypass and never resolve a per-user identity, so a token presented there is ignored.
  • The agent has a database connection. The OIDC validator and the SCIM role resolver register at startup only if one is available. Without it, no validator registers and a presented token is ignored (least-privilege attribution) rather than honored — the agent logs a rate-limited warning saying exactly this.
  • sso:configure permission for the operator doing the setup (steps 3–4).
Self-hosted (in-vpc) note

On a self-hosted (in-vpc) deployment the SSO config is stored under a platform-wide tenant key while the fleet verifier looks it up under your real licensed org. Platform v9.11.0 reconciles the two automatically on upgrade, provided the agent can identify the deployment org: set ORG_ID to your licensed org before upgrading. If the upgrade cannot verify the org it leaves the config untouched and logs a WARNING rather than guessing — in which case Path B stays inactive. After upgrading, confirm with:

SELECT tenant_id, org_id FROM sso_configurations;
-- in-vpc: expect tenant_id = '__platform__' and org_id = <your licensed org>

Path A is unaffected by any of this.

1. Configure SCIM provisioning first

Path B resolves roles from the SCIM-synced directory, so the directory must be populated before any token will resolve to more than least privilege. Group→role mappings only take effect for users SCIM has actually provisioned (mapping a group syncs its current members' roles). Do this first, or every developer authenticates successfully and then reads nothing.

  1. Create a SCIM bearer token in the portal (Settings → SCIM → Create Token; the page is titled Platform SCIM Provisioning on in-vpc) — copy it once; it is not shown again.
  2. Point JumpCloud's SCIM / directory-sync at $PORTAL_URL/scim/v2 using that token, and assign your developer group(s) so users and groups sync.

Full walkthrough: SCIM Getting Started · Group to Role Mapping.

2. Create the OIDC application in JumpCloud

  1. JumpCloud Admin Console → SSO Applications → Add New Application → Custom OIDC App.
  2. Grant types: Authorization Code (plus Refresh Token as desired). Redirect URI: your token-delivery tooling (device-flow / CLI helper).
  3. Note the values:
    • Issuer: https://oauth.id.jumpcloud.com/
    • JWKS URI: https://oauth.id.jumpcloud.com/.well-known/jwks.json
    • Audience: your app's client ID (or the custom audience you configure).
  4. Ensure the email claim is included (standard scopes: openid email).
  5. Assign the app to your developer group(s).
Verify against your own tenant's discovery document

Read your IdP's OIDC discovery document — for JumpCloud, https://oauth.id.jumpcloud.com/.well-known/openid-configuration — rather than copying the values above. IdPs vary the issuer (trailing slash included), the JWKS URI, and the scopes needed to emit email. A mismatched iss or aud is rejected fail-closed, which looks identical to "tokens don't work".

3. Configure AxonFlow (per tenant, requires sso:configure)

curl -X POST "$PORTAL_URL/api/v1/sso/config" \
-H "Cookie: <portal session>" \
-H "Content-Type: application/json" \
-d '{
"provider": "oidc",
"enabled": true,
"oidc_issuer": "https://oauth.id.jumpcloud.com/",
"oidc_audience": "<your client id>",
"oidc_jwks_uri": "https://oauth.id.jumpcloud.com/.well-known/jwks.json",
"oidc_claim_mapping": {"email": "email"}
}'
  • POST creates; it returns 409 if a config already exists. To change an existing one — including switching a tenant from SAML to OIDC — use PUT /api/v1/sso/config with the same body. (PATCH toggles enabled; DELETE removes the config.)
  • Issuer and JWKS URI must be https (plaintext HTTP is refused except loopback, because a swapped JWKS would turn validation into a forgery oracle). URLs targeting an internal or cloud-metadata endpoint (169.254.169.254, RFC-1918 hosts, *.internal) are rejected — the platform fetches these server-side.
  • Use a per-tenant issuer, not a shared multi-tenant "common" issuer. The verifier trusts the identity in a token signed by the configured issuer + audience; if two AxonFlow tenants configure the same issuer/audience, a token minted for one is structurally valid for the other. Give each tenant a distinct OIDC app (distinct issuer or audience) so a token cannot cross a tenant boundary.
  • oidc_claim_mapping.email names the claim carrying the developer identity (default email; e.g. preferred_username for some IdPs).
  • The SSO-config mutation endpoints require the sso:configure permission.

4. Map SCIM groups to roles

Path B roles come from the SCIM directory (step 1), never from the token. Map each synced JumpCloud group to an AxonFlow role. Both calls are session-authenticated and gated on sso:configure — deliberately not reachable with a SCIM directory-sync token, so a sync token cannot grant its own group roles.

First discover the role UUIDs:

curl "$PORTAL_URL/api/v1/scim/roles" -H "Cookie: <portal session>"
# => {"roles":[{"id":"3f2b…","name":"developer","display_name":"Developer",…}, …],
# "count":6}

Every org has the six fleet-mappable system roles seeded automatically: admin, owner, policy_admin, developer, member, viewer. New orgs get them on creation; existing orgs are backfilled on upgrade. These are the only role names a SCIM group may map to — the fleet resolver keys on the role name, so mapping a group to a differently-named custom role is rejected (400) rather than silently resolving every member to least privilege. Map each group to whichever of the six fits: admin or owner for a group that should read the whole tenant, developer / member / viewer for own-rows access.

Then map a group. The <group-id> is the SCIM group id — list them from the SCIM router with the SCIM bearer token (a different router and credential to the session-authenticated calls here): curl "$PORTAL_URL/scim/v2/Groups" -H "Authorization: Bearer $SCIM_TOKEN".

curl -X PUT "$PORTAL_URL/api/v1/scim/groups/<group-id>/role-mapping" \
-H "Cookie: <portal session>" \
-H "Content-Type: application/json" \
-d '{"role_id": "3f2b…"}'
# => {"group_id":"…","role_id":"3f2b…","role_name":"Developer","users_updated":12}

users_updated counts the group members whose role assignments were re-synced. A 0 means the mapping applied to nobody — an empty group, members SCIM has not provisioned yet (revisit step 1), or members that failed to resolve. It is not by itself an error, but on a group you expect to have developers in, treat it as one. Send {"role_id": null} to clear a mapping, and review current mappings with GET /api/v1/scim/groups/role-mappings.

A developer in no mapped group resolves to least privilege (own-rows read scope), never admin.

5. Deliver the token to the fleet

The IdP token travels on the fleet request as X-User-Token:

X-User-Token: <the developer's OIDC token>
Use X-User-Token everywhere

Authorization: Bearer <token> is accepted only on the MCP-server plane, as a concession to deployments that authenticate the tenant out-of-band. On the agent-proxied REST plane (the audit / decision / override read surfaces), Authorization is only ever the tenant credential — a Bearer per-user token there is parsed as a tenant credential and rejected. X-User-Token is the one header both planes read, so it is the only correct answer for a fleet.

The plugins send this header for you when AXONFLOW_USER_TOKEN is set; the fleet-scale delivery note under Path A applies unchanged to Path B tokens.

Send an ID token, not (necessarily) an access token

AxonFlow requires both an email claim and an aud matching the configured audience. Those are reliably present on ID tokens; many IdPs issue access tokens that are opaque, carry a resource-server aud, or omit email entirely — any of which is rejected fail-closed. Configure your token-delivery tooling to hand AxonFlow a token type that carries both, and confirm against a decoded sample before rolling out to the fleet.

What AxonFlow enforces on every OIDC token

  • RS256 only — alg: none and HS256 algorithm-confusion tokens are rejected before any key material is consulted.
  • iss exact match; aud must contain the configured audience; exp required and enforced; nbf honored (60s clock-skew leeway).
  • email_verified, when present, must be true — an IdP that emits a self-asserted / unverified email (e.g. multi-tenant Azure AD) cannot be used to impersonate another directory user's identity, and thus their role.
  • JWKS cached 15 minutes; an unknown kid triggers a rate-limited refetch, so IdP signing-key rotation is picked up automatically.
  • Every failure mode is fail-closed: unknown key, expired token, wrong audience, or an unreachable role directory means the request is not authenticated.

What the token unlocks: role-scoped reads

Both paths produce the same validated {identity, role}, and the fleet plane's validator accepts either. Once resolved, that role decides which cross-user governance rows the caller may read — audit events, decision history, and policy overrides — whether they arrive through the MCP read tools or a direct API call:

Resolved roleAudit / decision / override reads
admin / ownerFull tenant — every user's rows
developer, member, viewer, policy_adminOwn rows only — rows attributed to their own identity
No per-user identity (shared credential alone)Zero rows — fail-closed

Two properties worth restating alongside provisioning:

  • Emails are canonicalized (lower-cased, trimmed) at mint, validation, and revocation, so audit attribution and read-scoping key on the same value — a developer always sees the full set of their own attributed history.
  • Path B roles come from the SCIM directory, never the token. A developer with no mapped role resolves to least privilege (own-rows), so an IdP misconfiguration can never mint an admin.

The complete model — the enforcement layers, the covered surfaces, admin-gated compliance exports, and the v9.10.0 behavior change for token-less callers — is documented in Per-Developer Identity → Role-scoped reads.