Client Credentials Flow

The Client Credentials Grant (RFC 6749 §4.4) is for machine-to-machine (M2M) authentication. No user is involved — the service authenticates as itself with a client_id and client_secret.

When to Use This Flow

  • A microservice, daemon, or CI/CD pipeline calls a protected API
  • There is no user — pure service identity
  • The service can securely store a client_secret (server-side secrets manager, env vars — never a browser, mobile app, or CLI distributed to end users)

Before You Integrate

Ask your Signet administrator to create a confidential client with:

  • Client Credentials Flow enabled
  • The specific scopes the service needs (custom API scopes defined for your deployment)
  • No redirect URIs required

You’ll receive:

  • client_id — safe to log
  • client_secret — store in a secrets manager; rotate if leaked

Restricted scopes: openid and offline_access are not valid in this flow and will be rejected with invalid_scope. Scopes are mapped to a synthetic service identity, not a user.

How It Works

sequenceDiagram participant Service participant Signet participant API Service->>Signet: POST /oauth/token (grant_type=client_credentials, Basic auth) Signet-->>Service: access_token + expires_in note over Service: Cache token, refresh when near expiry Service->>API: GET /resource (Authorization Bearer) API->>API: Verify JWT locally via JWKS API-->>Service: 200 OK note over Service: Token expires, request a new one (no refresh token)

Step 1: Request an Access Token

Authenticate via HTTP Basic Auth (preferred) or form body. Only application/x-www-form-urlencoded bodies are accepted.

HTTP Basic (recommended per RFC 6749 §2.3.1):

curl -X POST https://your-signet/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials"

Form body:

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"

Omit scope to receive the full set of scopes registered on your client. Include scope=... only when you want to request a subset of them.

With Resource Indicators (RFC 8707):

curl -X POST https://your-signet/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "resource=https://api.example.com"

resource is optional, repeatable (max 10), must be an absolute http(s) URI without a fragment, ≤ 1024 chars. When supplied, the issued JWT’s aud claim is bound to those resources — your resource server validates aud against its own identifier (see JWT Verification §Audience Binding). Each value must be on your client’s allowed-resources allowlist, which is deny-all by default: a client with an empty allowlist gets 400 invalid_target for any resource= it sends — ask your admin to allowlist the resource identifiers this service targets. Without resource, aud falls back to the deployment-wide JWT_AUDIENCE. Malformed or not-allowlisted values return 400 invalid_target — see Errors.

Multi-RS deployments: if one service calls several resource servers with distinct identifiers, request a separate token per resource (each cached independently). Sharing one token across resource servers defeats audience binding — any RS the token is valid for becomes a relay point for the others.

The $CLIENT_SECRET env-var form above is fine in docs; in production, never pass a literal secret on the command line — argv is visible via ps and shell history. Pipe from curl --netrc, a config file (-K), or use a language SDK.

Response:

{
  "access_token": "eyJhbG...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "<scopes granted to this client>"
}

No refresh token is issued for this grant type (RFC 6749 §4.4.3). When the access token expires, request a new one. /oauth/revoke and /oauth/tokeninfo still apply — see Tokens & Revocation.

If you request a scope the client isn’t permitted for, Signet returns:

{
  "error": "invalid_scope",
  "error_description": "Requested scope exceeds client permissions or contains restricted scopes (openid, offline_access are not permitted)"
}

See Errors for the full catalog.

Step 2: Use the Token

curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.example.com/resource

Resource servers should verify the JWT locally using Signet’s JWKS — see JWT Verification.

Identifying M2M tokens: the JWT sub (and user_id) claim has the form client:<client_id> for Client Credentials tokens. Your resource server can branch on this to distinguish service calls from user-delegated calls. The aud claim is the resource indicator you requested (or JWT_AUDIENCE fallback) — validate it against the RS’s own identifier just like for user tokens.

Step 3: Cache and Renew

Don’t fetch a new token on every request. Cache it in memory and refresh only when nearing expiry (subtract a safety margin):

// Go pseudo-code
if time.Now().Add(30 * time.Second).After(expiresAt) {
    accessToken, expiresAt = requestNewToken()
}
# Python
if time.time() + 30 >= expires_at:
    access_token, expires_at = request_new_token()

Avoid a thundering herd across service replicas: add small random jitter to the 30-second buffer, or use a shared cache (Redis) with a single-flight renewal.

Security Checklist

Requirement Details
Store secrets securely Secrets manager or env vars injected at runtime — never commit to source control
Use HTTPS The client_secret crosses the wire on every token request
One client per service Independent revocation and per-service scope control
Request only needed scopes Principle of least privilege
Rotate on compromise Ask the admin to regenerate the secret; update your secrets manager
Retry with backoff /oauth/token is rate-limited — see Tokens & Revocation §Rate Limits; handle 429 with Retry-After
Cache token, don’t re-fetch Respect expires_in; only renew when close to expiry
Monitor audit logs Ask your admin to set up alerts on anomalous CLIENT_CREDENTIALS_TOKEN_ISSUED events