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:
openidandoffline_accessare not valid in this flow and will be rejected withinvalid_scope. Scopes are mapped to a synthetic service identity, not a user.
How It Works
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_SECRETenv-var form above is fine in docs; in production, never pass a literal secret on the command line — argv is visible viapsand shell history. Pipe fromcurl --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/revokeand/oauth/tokeninfostill 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 |