feat(token-handler): distinguish public and confidential clients #81#457
feat(token-handler): distinguish public and confidential clients #81#457dsschiramm wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the token endpoint client-authentication behavior to distinguish public vs confidential OAuth clients (RFC 6749 §4.1.3), allowing public clients to omit client_secret while keeping a fail-closed default for clients without an explicit type.
Changes:
- Move the “client_secret required” decision to after
model.getClient()so it can be based onclient.type. - Extend the model/client contract to include
type?: 'public' | 'confidential'(defaulting to confidential behavior when unset). - Add/adjust unit and integration tests covering public vs confidential clients with missing
client_secret.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/handlers/token-handler.js | Defers secret requirement check until after client lookup; adds client.type handling. |
| lib/model.js | Documents the new ClientData.type attribute. |
| index.d.ts | Adds type?: 'public' | 'confidential' to the Client TypeScript interface. |
| test/integration/handlers/token-handler_test.js | Adds integration coverage for public/confidential behavior when client_secret is omitted. |
| test/unit/handlers/token-handler_test.js | Adds a unit assertion that model.getClient() is called with null when the secret is omitted. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…trieval and rename test description
| }; | ||
| } | ||
|
|
||
| if (pkce.isPKCERequest({ grantType, codeVerifier })) { |
There was a problem hiding this comment.
Can you please elaborate why this is now fully omitted? The role of PKCE is especially to protect public clients that cannot safely keep a secret.
There was a problem hiding this comment.
The !isPkce condition has been relocated. Previously, getClientCredentials had two early-return flows (one for PKCE requests and another for !isClientAuthenticationRequired) that both returned { clientId } without the secret. I consolidated both into a single return, because the decision regarding whether the secret is required or if the PKCE/public-client status excuse it, now take place in getClient after model.getClient() executes, since that's the only place client.type (public vs confidential) is known. Therefore, the check has moved but has not disappeared: getClient still evaluates isClientAuthenticationRequired(grantType, client) && !credentials.clientSecret && !isPkce before accepting a client without a secret, and isClientAuthenticationRequired checks if client is public.
There was a problem hiding this comment.
This change in the validation location is also the reason why I unified the error with the !client case: both throw the same InvalidClientError('Invalid client: client is invalid'). Otherwise a client_id that exists but lacks its required secret would be distinguishable from one that doesn't exist.
|
I'm generally positive for this but we need to keep a close eye on the PKCE. What do you think @dhensby? |
Summary
RFC 6749 4.1.3 requires public clients be allowed to omit client_secret at the token endpoint. getClientCredentials() previously rejected requests missing a secret before the model was ever consulted, so client.type could never be checked. Moved checking the secret requirement to after the client is fetched so it can be decided based on client.type.
Obs.: Unified the error message to prevent leaking whether a client exists. Both unknown clients and clients missing their required secret now return the same InvalidClientError("Invalid client: client is invalid").
Linked issue(s)
#81
Involved parts of the project
Added tests?
OAuth2 standard
RFC 6749 §4.1.3 (Access Token Request) — public clients are not required to authenticate with a client_secret; confidential clients are. This PR adds an optional client.type ('public' | 'confidential') to the model contract, defaulting to 'confidential' when unset (fail-closed — existing integrations that never set type keep requiring a secret, no behavior change for them).
Reproduction