Skip to content

fix(server)!: admit only confidential clients to client_credentials - #83

Merged
KyleJune merged 2 commits into
mainfrom
fix/client-credentials-confidential
Oct 7, 2026
Merged

KyleJune merged 2 commits into
mainfrom
fix/client-credentials-confidential

Conversation

@KyleJune

@KyleJune KyleJune commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Why

RFC 6749 §4.4: "The client credentials grant type MUST only be used by confidential clients." ClientCredentialsGrant issued a token to any client that getAuthenticated resolved, and that interface resolves a public client presenting no secret, so a public client registered for client_credentials got a machine token. The fake tenant in @udibo/oauth2/testing already refuses that request with 401 invalid_client; the real grant now does the same.

The grant decides from the credentials, not from a client field: ClientInterface carries no confidentiality flag. getAuthenticatedClient now refuses a request whose credentials (read through getClientCredentials, HTTP Basic or body) carry no non-empty client_secret before any lookup, then authenticates through clientService.getAuthenticated as before. That interface already refuses a public client presenting a secret and a confidential client with a missing or wrong one (runClientServiceContractTests pins both), so a request passes only when a confidential client authenticated with its secret. An empty secret, including a Basic header of client_id:, counts as none.

getAuthenticatedClient and getClientCredentials stay overridable. A subclass that calls super.getAuthenticatedClient keeps the check; one that authenticates another way (a signed client assertion) replaces the method and owns the guarantee, as the JSDoc says. The provider and local-IdP guides now state the rule.

Verification

  • src/server/grants/client-credentials.test.ts → "client authentication" drives AuthorizationServer.handleTokenRequest. Against the unchanged grant, four refusals failed by issuing a 200 token: a public client presenting only client_id, an empty client_secret, Basic with an empty password, and a request with no secret where the client service would resolve the client anyway. All pass now. The confidential-client-without-its-secret and public-client-with-a-stray-secret cases pass before and after (the client service refuses them), and a confidential client with its secret still gets a token by body and by Basic. "refuses a public client whose Basic password is empty even when the body carries a client_secret" pins that the check reads the same credentials getAuthenticated receives (Basic wins over the body): it fails with a 200 token when the check also accepts a body client_secret, and passes at this change.
  • authorization-server.test.ts → "answers invalid_client when a public client presents a secret it was never issued" used a public client's 200 on client_credentials as its control. It now uses refresh_token, where a public client authenticating with none is still accepted (400 invalid_grant for the unknown token), so it still separates authenticating with none from presenting an unissued secret.
  • deno task check exit 0; deno task test:all exit 0 (package 239 passed / 2998 steps, plus scripts, examples, templates).

Risk

Door: two-way in code; the release it cuts is published. A deployment that serves client_credentials to clients without a secret gets 401 invalid_client after upgrading. Confidential clients and every other grant are unchanged.

Closes

Nothing in this repository.

BREAKING CHANGE: ClientCredentialsGrant refuses a token request that presents no client secret with 401 invalid_client. Register machine clients as confidential and authenticate them with their client secret, by HTTP Basic or client_secret in the body. A client service written before 0.9.2 must pass runClientServiceContractTests, or client_credentials is not limited to confidential clients.

🤖 Generated with Claude Code

KyleJune and others added 2 commits October 6, 2026 21:49
RFC 6749 section 4.4: the client credentials grant type MUST only be
used by confidential clients. ClientCredentialsGrant now refuses a token
request that presents no client secret with 401 invalid_client.

BREAKING CHANGE: register machine clients as confidential and
authenticate them with their client secret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also document that a client service must pass
runClientServiceContractTests for the grant to admit only confidential
clients.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KyleJune

KyleJune commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

test(server): pin an empty Basic password over a body client_secret — adds a test that a public client sending Basic client_id: plus a body client_secret is refused 401 invalid_client, so the check reads the same credentials getAuthenticated receives (it fails with a 200 under a check that also accepts the body secret). The grant's JSDoc and the BREAKING CHANGE note now say a client service written before 0.9.2 must pass runClientServiceContractTests for client_credentials to be limited to confidential clients. check and test:all green.

@KyleJune
KyleJune merged commit 638123e into main Oct 7, 2026
12 checks passed
KyleJune pushed a commit that referenced this pull request Oct 7, 2026
## [0.13.0](0.12.2...0.13.0) (2026-10-07)

### ⚠ BREAKING CHANGES

* **server:** `ClientCredentialsGrant` refuses a token request that presents no client secret with 401 `invalid_client`. Register machine clients as confidential and authenticate them with their client secret, by HTTP Basic or `client_secret` in the body. A client service written before 0.9.2 must pass `runClientServiceContractTests`, or `client_credentials` is not limited to confidential clients.

### Bug Fixes

* **server:** admit only confidential clients to client_credentials ([#83](#83)) ([638123e](638123e))
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 0.13.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant