Client Credentials
grant_type=client_credentials is the service-to-service grant: no end user, no browser, no consent. A confidential backend authenticates as itself and gets an access token to call another backend.
Specs referenced on this page
- RFC 6749 — OAuth 2.0 Authorization Framework (§4.4 client credentials)
- RFC 7521 — Assertion Framework for OAuth 2.0
- RFC 7523 — JWT Profile for OAuth 2.0 Client Authentication (
private_key_jwt) - RFC 7800 — Confirmation (
cnf) claim - RFC 8705 — Mutual-TLS Client Authentication
- RFC 9068 — JWT Profile for OAuth 2.0 Access Tokens
Vocabulary refresher
- Confidential client — a client that can hold a real authentication credential (a long-lived secret, an asymmetric key, or an X.509 certificate). Backends and trusted servers are confidential.
- Public client — a client that cannot hold a real secret in practice (SPAs in a browser, native mobile apps). They authenticate with
token_endpoint_auth_method=noneand rely on PKCE for code protection. private_key_jwt— client authentication where the client signs a short-lived JWT with its private key; the OP verifies it against the registered public key. Stronger than a shared secret because the secret never leaves the client.
Confidential clients only
client_credentials is structurally restricted to confidential clients in this library — clients with a real authentication credential (client_secret_basic, client_secret_post, private_key_jwt, tls_client_auth, self_signed_tls_client_auth). Public clients (SPAs / native, token_endpoint_auth_method=none) cannot use it.
There is no end user, so there is no PKCE, no consent, no id_token, and no refresh token. The client just re-runs the grant when the access token expires.
Wiring
Enable the grant explicitly. The library refuses to mint tokens for any grant type not in the enabled set.
import (
"github.com/libraz/go-oidc-provider/op"
"github.com/libraz/go-oidc-provider/op/grant"
)
handler, err := op.New(
op.WithIssuer("https://op.example.com"),
op.WithStore(inmem.New()),
op.WithKeyset(myKeyset),
op.WithCookieKey(cookieKey),
op.WithGrants(
grant.AuthorizationCode, // for human users
grant.RefreshToken,
grant.ClientCredentials, // for service-to-service
),
op.WithStaticClients(/* a confidential client */),
)A registered client opting in via GrantTypes:
op.WithStaticClients(op.ConfidentialClient{
ID: "service-a",
Secret: "rotate-me",
AuthMethod: op.AuthClientSecretBasic, // or use op.PrivateKeyJWTClient instead for private_key_jwt
GrantTypes: []string{"client_credentials"},
Scopes: []string{"read:things", "write:things"},
})Why both global and per-client opt-in?
- Global (
op.WithGrants) gates which grants the OP supports at all, surfaced ingrant_types_supportedof the discovery document. - Per-client (
store.Client.GrantTypes) gates which of those grants this specific client may use.
A client only succeeds when both checks pass. This lets you run a single OP that issues authorization_code for end-user apps and client_credentials for backend services, without giving the SPA client the ability to mint app-level tokens.
Token shape
client_credentials access tokens carry:
iss— the OP issueraud— the resource server identifier (the typed seeds carry aResources []stringfield that lists permitted RFC 8707 resource indicators; with DCR, the equivalentresourcesmetadata field)client_id— the requesting client- No
subfor purely-machine clients (orsub = client_idif you setact_as_subject) scope— the granted subset of the requested scopes
When op.WithFeature(feature.MTLS) or DPoP is configured and the client presented a sender constraint, the token additionally carries the cnf (Confirmation, RFC 7800) claim binding it to that constraint.
See it run
examples/05-client-credentials ships a runnable end-to-end version with a client_secret_basic client.
go run -tags example ./examples/05-client-credentialsRead next
- Sender constraint (DPoP / mTLS) — bind service tokens to a client-held key so a stolen token is useless.
- Use case: client_credentials in production.