Skip to content

feat(oauth2): add client credentials flow with opt-in config flag#4583

Merged
nabokihms merged 18 commits intodexidp:masterfrom
matzegebbe:feat/add-client-credential-flow
Mar 3, 2026
Merged

feat(oauth2): add client credentials flow with opt-in config flag#4583
nabokihms merged 18 commits intodexidp:masterfrom
matzegebbe:feat/add-client-credential-flow

Conversation

@matzegebbe
Copy link
Copy Markdown
Contributor

Overview

Add OAuth2 client_credentials grant type support, gated behind an opt-in clientCredentialsEnabled config flag.

What this PR does / why we need it

Dex has no built-in support for the client_credentials grant, which is the standard OAuth2 flow (RFC 6749 Section 4.4) for service-to-service authentication where no end-user is involved. This has been a long-standing community request.

This PR implements the grant and gates it behind a new clientCredentialsEnabled config flag that defaults to false, following the same pattern used by passwordConnector for the password grant. This ensures no behavior change for existing deployments.

Grant behavior:

  • Requires a confidential client (public clients are rejected)
  • Authenticates via client ID + secret (HTTP Basic auth)
  • Supports openid, email, profile, groups scopes
  • Returns an ID token when openid scope is requested
  • Rejects offline_access and federated:id scopes (no refresh tokens for M2M)
  • Claims are derived from the client itself: aud = client ID, sub = encoded client ID + connector ID, name/preferred_username = client name (with profile scope)

Configuration:

oauth2:
  clientCredentialsEnabled: true

Security considerations:

  • Opt-in only: the grant is never exposed unless explicitly enabled
  • Confidential clients only: public clients cannot use this grant
  • No refresh tokens: offline_access scope is rejected, limiting token lifetime
  • Service identity: the sub claim is the client ID, not a user

Testing:

  • go build ./... compiles cleanly
  • go test ./server/... -run TestHandleClientCredentials — 7 cases pass
  • go test ./server/... -run TestServerSupportedGrants — 6 cases pass
  • go test ./server/... -run TestHandleDiscovery — passes
  • go test ./server/... — full server suite passes
  • Manual test: start dex with clientCredentialsEnabled: true, request token via curl

Manual verification:

  1. Enable the flag in examples/config-dev.yaml:
oauth2:
  clientCredentialsEnabled: true
  1. Build and run dex:
go run ./cmd/dex serve examples/config-dev.yaml
  1. Request an access token:
curl -X POST http://127.0.0.1:5556/dex/token \
  -u "example-app:ZXhhbXBsZS1hcHAtc2VjcmV0" \
  -d "grant_type=client_credentials"
  1. Request an access token + ID token:
curl -X POST http://127.0.0.1:5556/dex/token \
  -u "example-app:ZXhhbXBsZS1hcHAtc2VjcmV0" \
  -d "grant_type=client_credentials&scope=openid+profile"

Closes #3660

The gating mechanism works the same way as the password grant: client_credentials is included in the default allowed grant types list, but only gets added to allSupportedGrants (and thus passes the intersection filter in newServer()) when ClientCredentialsEnabled is true. Without the flag, the grant type is silently filtered out.

@matzegebbe matzegebbe force-pushed the feat/add-client-credential-flow branch 3 times, most recently from 370999f to fab39be Compare February 25, 2026 13:43
Copy link
Copy Markdown
Member

@nabokihms nabokihms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! There are some points to resolve before merging.

@nabokihms nabokihms added the release-note/new-feature Release note: Exciting New Features label Feb 25, 2026
@matzegebbe matzegebbe force-pushed the feat/add-client-credential-flow branch 2 times, most recently from 0befb94 to 0b64c08 Compare February 25, 2026 20:11
matzegebbe and others added 15 commits February 25, 2026 22:52
Implement the OAuth2 client_credentials grant type for
machine-to-machine authentication. The grant is gated behind a new
clientCredentialsEnabled config flag (defaults to false), following
the same pattern as passwordConnector for the password grant.

Closes dexidp#3660

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
The gating happens via allSupportedGrants in server.go, not via the
allowed list. Without client_credentials in the defaults, the
intersection filter always excluded it even with the flag enabled.
This matches how the password grant works: present in defaults but
only activated when the corresponding config flag is set.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
…onfig flag

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
…lient_credentials

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
The __client_credentials connector ID is no longer used since the
client_credentials grant now uses an empty connector ID. Remove the
__ prefix validation from CreateConnector and its associated test.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
…ials

Use an empty connector ID instead of __client_credentials to avoid
requiring reserved ID validation. Read the nonce parameter from the
token request and forward it to newAccessToken and newIDToken.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Add DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT feature flag
(default false) so client_credentials is not advertised by default.
Users can still explicitly enable it via oauth2.grantTypes config.
The flag will be flipped to true in a future release before removal.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
…lag is enabled

The serve command sets a default grantTypes list when none is configured,
which meant AllowedGrantTypes was never empty and the feature flag check
in server.go was bypassed. Append client_credentials to the default list
when DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT is true.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
The feature flag check in the else branch of server.go is dead code
since serve.go always sets a default AllowedGrantTypes list. Move the
gate entirely to cmd/dex/serve.go and remove the unused featureflags
import. Restore server_test.go to match server.go behavior directly.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
@matzegebbe matzegebbe force-pushed the feat/add-client-credential-flow branch from 40f46d7 to 2dcd9b9 Compare February 25, 2026 21:52
@nabokihms nabokihms self-requested a review February 26, 2026 08:01
Signed-off-by: Maksim Nabokikh <maksim.nabokikh@flant.com>
Signed-off-by: Maksim Nabokikh <max.nabokih@gmail.com>
Copy link
Copy Markdown
Member

@nabokihms nabokihms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sagikazarmark, since the feature is opt-in and not enabled by default, I see no problem merging this. If you have no objections, I will merge the PR.

@matzegebbe thank you for the PR.

Signed-off-by: Maksim Nabokikh <maksim.nabokikh@flant.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/new-feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement client credentials flow

2 participants