SenseCrypt Docs
Integrations

Machine-to-machine

Get an access token for the SenseCrypt Management API with the OAuth 2.0 client-credentials grant — register an M2M app, bind roles, call the token endpoint, and use the token.

Use the client-credentials grant when a backend service needs to call the SenseCrypt Management API with no user present — for example to automate tenant administration or drive SCIM provisioning programmatically. The result is an access token whose capabilities come from the roles bound to the machine app.

Two different things share this grant. An M2M app (a service account, client_id of the form m2m_…) mints a token for the tenant's Management API, audience {issuer}/v1/admin/ — that is what this guide covers. A registered OIDC application can also use client_credentials to mint a token for one of your resource servers, but only when Allow the client credentials grant (allow_client_credentials) is turned on for it — it is off by default. That path has its own contract, and OIDC & OAuth 2.0 → Grant types is where it is written down; nothing on this page applies to it. client_credentials is listed in grant_types_supported on Standard and FAPI issuers alike.

Prerequisites

  • A SenseCrypt tenant.
  • An M2M app created in the admin console, with one or more M2M roles bound to it.

1. Create an M2M app and bind roles

In the admin console, create an M2M app. You receive a client_id (of the form m2m_...) and a client_secret (store the secret securely — treat it like any credential). In a FAPI 2.0 tenant you can instead give the app a key or a certificate and no secret at all — see FAPI 2.0 tenants below; the rest of this section applies either way. Bind the roles that grant the capabilities this service needs; the token's capabilities are the union of its bound roles, recomputed on every mint, so role changes take effect on the next token.

2. Request a token

The M2M app is tenant-scoped, so request a token from the tenant's issuer token endpoint (resolve it from discovery; conventionally /v1/idp/oidc/token). The requested audience must be the tenant's Management API identifier — the exact value {issuer}/v1/admin/ (note the trailing slash):

curl -X POST {token_endpoint} \
  -d grant_type=client_credentials \
  -d client_id={client_id} \
  -d client_secret={client_secret} \
  -d audience={issuer}/v1/admin/

Successful response:

{
  "access_token": "eyJhbGciOiJFUzI1Ni…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "oidc_apps:read auth_users:create …"
}

The ES256 access token carries:

  • sub in the form {client_id}@clients,
  • azp = your client_id, and gty = client-credentials,
  • scope and a permissions claim = the capabilities of the app's bound roles,
  • token_use: "access", a jti, and an expiry from the app's configured access-token lifetime (3600s by default).

If the audience is not the tenant's Management API identifier, the request is rejected with invalid_target ("Audience must be the management API identifier"). Both audience (Auth0 spelling) and resource (RFC 8707) are accepted; if you send both, resource wins.

3. Call the Management API

Send the access token as a bearer credential to the Management API:

curl {management_api_url} \
  -H "Authorization: Bearer {access_token}"

Each capability is a fine-grained entity:verb (for example oidc_apps:read, auth_users:create). The token can only act within the one tenant it was minted for, and only up to the capabilities its roles carry.

Audience string vs. call host. The audience you request is the Management API identifier {issuer}/v1/admin/ (built from the issuer host). The admin API itself is served on your tenant's admin host, which may differ from the issuer host. Request the audience exactly as {issuer}/v1/admin/, but confirm the actual {management_api_url} admin host with your operator. See the API Reference for the admin endpoints.

End to end, the three steps chain from token mint to an authorized Management API call:

POST /v1/idp/oidc/tokengrant_type=client_credentials, client_id, client_secret,audience={issuer}/v1/admin/ Resolve M2M app in THIS tenant;verify its credential (with rotation grace) audience must equal the management identifierelse invalid_target permissions = union of bound M2M roles' capabilities(recomputed on every mint) Sign ES256 token: sub={client_id}@clients, azp,gty=client-credentials, scope, permissions, token_use=access 200 { access_token, token_type: Bearer, expires_in, scope } {management_api_url} + Authorization: Bearer {access_token} Verify ES256 signature + aud;enforce entity:verb capability gate 200 authorized — or 403 if a required capability is missing Throttle per IP + client_id before any crypto/DB work Backend service (M2M client) Token endpoint Management API

FAPI 2.0 tenants: sign in with a key or a certificate

In a FAPI 2.0 tenant the create dialog also offers three ways to sign in without a secret. Secret-based apps stay available beside them and work as before, but their tokens are plain bearer tokens: anyone who holds one can call the Management API with it until it expires. The console's M2M apps page marks each of them Secret · Bearer in a FAPI 2.0 tenant and counts the enabled ones in one notice. To move one, see Move a secret app to a key or certificate.

Sign-in methodYou registerAt /token the app presents
Private key (private_key_jwt)One public key (JWK), ES256 on P-256 or PS256 on RSA ≥ 2048 bits, with alg setA client_assertion signed with its private key: iss = sub = client_id, aud = the issuer string, a fresh jti, a short exp
Certificate from your CA (tls_client_auth)Your issuing CA certificate(s) and the subject DN the app's certificate carriesIts client certificate, over the mutual TLS token endpoint (mtls_endpoint_aliases.token_endpoint in discovery)
Self-signed certificate (self_signed_tls_client_auth)The app's own certificateThat certificate, over the mutual TLS token endpoint

Every token such an app gets is sender-constrained. You choose how when you create it:

  • DPoP key (the default for a private key). Send a DPoP proof with every token request. The token comes back with token_type: DPoP and a cnf.jkt, and every Management API call must use Authorization: DPoP <token> plus a fresh proof for that call.
  • Client certificate (the default for a certificate). The token carries cnf.x5t#S256 and works only on a mutual TLS connection that presents the same certificate, so call the Management API through the mutual TLS listener too.
# Private key + DPoP
curl -X POST {token_endpoint} \
  -H "DPoP: <proof: htm=POST, htu={token_endpoint}>" \
  -d grant_type=client_credentials \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion=<JWT signed with the app's private key> \
  -d audience={issuer}/v1/admin/

# Certificate, bound to the certificate
curl -X POST --cert client.crt --key client.key {mtls_token_endpoint} \
  -d grant_type=client_credentials \
  -d client_id={client_id} \
  -d audience={issuer}/v1/admin/

The first DPoP request is answered 400 use_dpop_nonce with a DPoP-Nonce header. Retry with a proof that carries that nonce. For a private-key app, also send a new client_assertion: the first one's jti has already been used, and a replayed jti is invalid_client. Each successful token response carries a fresh DPoP-Nonce for the next request.

The app's keys and certificates rotate without an outage. The new one works at once, and the old one keeps working for 24 hours or stops now. See Rotating client keys and certificates.

Move a secret app to a key or certificate

An app's sign-in method is set when the app is created and cannot be changed afterwards. To move a service off a client secret, give it a new app and retire the old one:

  1. Create a new app in the same tenant that signs in with a private key or a certificate (the table above). Bind the same roles and set the same token lifetime. The new app has its own client_id.
  2. Switch your service to the new client_id and sign-in method. This is a code change as well as a configuration change: a DPoP-bound token needs Authorization: DPoP <token> and a fresh proof on every Management API call, and a certificate-bound token works only over mutual TLS with the same certificate.
  3. Check that the old app is idle. Its last used time on the M2M apps page stops moving once nothing signs in with its secret.
  4. Disable the old app. It can no longer get a token, the Management API refuses the tokens it already holds, and the console notice stops counting it. Disabling can be undone, so you can turn it back on if something still depended on it.
  5. Delete the old app when you are sure. Deleting cannot be undone.

SCIM via M2M

An M2M role can also grant the SCIM capabilities (scim:read / scim:write). A token carrying those can call the SCIM endpoints directly — an alternative to an opaque ssct_ SCIM token.

Verify it worked

  • Decode the token (e.g. on jwt.io or with your JWT library) and confirm sub is {client_id}@clients, aud is {issuer}/v1/admin/, and permissions lists the capabilities you expect from the bound roles.
  • Make one read call you're authorized for (for example listing OIDC apps if the token has oidc_apps:read) and confirm a 200.
  • Call an endpoint your roles don't cover and confirm you're denied — that proves the capability gate is enforcing, not that your token is broken.

Troubleshooting

  • invalid_target. The audience isn't exactly {issuer}/v1/admin/ (watch the trailing slash and the issuer host).
  • invalid_client. Wrong client_id/client_secret, a disabled app, or a credential from a different tenant. All of these collapse to a single opaque invalid_client (HTTP 401) by design. For a FAPI app, the same answer covers a secret sent to an app that has none, an assertion signed with a key that isn't current (or still in its grace window), a replayed assertion jti, and a certificate that doesn't match the registration.
  • use_dpop_nonce / invalid_dpop_proof at /token. The app's tokens are DPoP-bound: send a proof, and retry once with the returned DPoP-Nonce. A private-key app also needs a new assertion for the retry.
  • 401 with kind: sender_constraint from the Management API. A bound token was sent without its proof or certificate: a DPoP-bound token as Bearer, without a DPoP header, or with a proof from another key; or a certificate-bound token off the mutual TLS listener or with another certificate. The WWW-Authenticate header says what was expected.
  • Authorized token, but a call returns 403. The token is valid but its bound roles don't include the capability that endpoint needs. Add the role in the console and mint a fresh token (capabilities are recomputed per mint).
  • No response at all from the mutual TLS token endpoint. A known cause is a client certificate that is not X.509 v3. See No response from a mutual TLS endpoint.
  • Calls 404 / connection fails. You may be calling the issuer host instead of the admin host. The audience string and the call URL can differ — see the note in step 3.

Notes

  • Client-credentials mints no refresh token and no ID token — request a fresh access token when it expires.
  • Secrets support a rotation grace window, so you can roll a secret without an outage. Keys and certificates do too: see Rotating client keys and certificates.
  • Introspection: an M2M app may introspect its own Management-API tokens.

On this page