diff --git a/docs/api/model.md b/docs/api/model.md index 600ba77..c5d420d 100644 --- a/docs/api/model.md +++ b/docs/api/model.md @@ -84,6 +84,14 @@ This model function is **required** for all grant types. - `refresh_token` grant - `password` grant +**Remarks:** +`clientSecret` is `null`/absent for public clients, and also for +`authorization_code` requests that authenticate with PKCE (a `code_verifier`) +instead of a secret — even if the client *has* a `client_secret`. PKCE is +**not** a substitute for client authentication: your implementation must +reject (return a falsy value) a confidential client that should have +presented its `client_secret` but did not. + **Kind**: instance method of [Model](#Model) **Fulfil**: [ClientData](#ClientData) - An `Object` representing the client and associated data, or a falsy value if no such client could be found. **Reject**: Error - An Error type diff --git a/docs/api/server.md b/docs/api/server.md index 5286687..5d8f3a6 100644 --- a/docs/api/server.md +++ b/docs/api/server.md @@ -158,7 +158,7 @@ function authorizeHandler(options) { Retrieves a new token for an authorized token request. **Remarks:** If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. -By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. +By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. Note: setting a grant to `false` disables only the *presence* check of the `client_secret` for that grant, and does so for **all** clients (not just public ones); it does not validate a secret that is sent. The same applies to `authorization_code` requests that use PKCE (a `code_verifier`), where the presence check is skipped. Per-client (public vs confidential) authentication must therefore be enforced in your `Model#getClient() ` implementation, which should reject a confidential client that fails to present its `client_secret`. ```js let options = { // ... diff --git a/docs/guide/pkce.md b/docs/guide/pkce.md index 3eace68..7e89658 100644 --- a/docs/guide/pkce.md +++ b/docs/guide/pkce.md @@ -130,3 +130,36 @@ The loaded code has to contain `codeChallenge` and `codeChallengeMethod`. If `model.saveAuthorizationCode` did not cover these values when saving the code then this step will deny the request. See `Model#saveAuthorizationCode` and `Model#getAuthorizationCode` + +## PKCE, client authentication and refresh tokens + +PKCE only protects the `authorization_code` → token exchange. The `code_verifier` +is sent and verified **once**, when the authorization code is redeemed (step 3 +above); it is **not** a parameter of the `refresh_token` grant and is ignored +there. A client that obtained its tokens via PKCE refreshes them like any other +client — by presenting its `refresh_token` (and, if it is a confidential client, +its `client_secret`). + +PKCE is **not** client authentication, and never a substitute for a `client_secret`: + +- A **confidential** client (one issued a `client_secret`) must authenticate with + its secret on **every** token request, including `refresh_token`. PKCE is + additive. Your `model.getClient(clientId, clientSecret)` is responsible for + rejecting (returning a falsy value) a confidential client that fails to present + its secret. +- A **public** client has no secret. If you choose to issue refresh tokens to + public clients (weigh the security implications first — see + [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700)), relax client authentication + for that grant: + + const server = new OAuth2Server({ + model, + requireClientAuthentication: { refresh_token: false } // allow refresh without a client_secret + }) + + `requireClientAuthentication: { refresh_token: false }` disables the + `client_secret` **presence** check for the `refresh_token` grant for **all** + clients, not just public ones, so per-client (public vs confidential) + enforcement must be done in your `model.getClient`. The library does not yet + model the public/confidential distinction itself (tracked in + [#81](https://github.com/node-oauth/node-oauth2-server/issues/81)). diff --git a/index.d.ts b/index.d.ts index e67540b..dddacaf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -228,6 +228,9 @@ declare namespace OAuth2Server { /** * Require a client secret. Defaults to true for all grant types. + * Setting a grant to `false` disables the client_secret presence check for + * that grant for ALL clients (not just public ones); per-client + * (public vs confidential) enforcement must be done in model.getClient. */ requireClientAuthentication?: Record; diff --git a/lib/model.js b/lib/model.js index b426aa9..ddda33f 100644 --- a/lib/model.js +++ b/lib/model.js @@ -98,6 +98,13 @@ class Model { // eslint-disable-line no-unused-vars * - `refresh_token` grant * - `password` grant * + * **Remarks:** + * `clientSecret` is `null`/absent for public clients, and also for + * `authorization_code` requests that authenticate with PKCE (a `code_verifier`) + * instead of a secret — even if the client *has* a `client_secret`. PKCE is + * **not** a substitute for client authentication: your implementation must + * reject (return a falsy value) a confidential client that should have + * presented its `client_secret` but did not. * * @async * @param clientId {string} The client id of the client to retrieve. diff --git a/lib/server.js b/lib/server.js index 320ddea..d9a6239 100644 --- a/lib/server.js +++ b/lib/server.js @@ -176,7 +176,7 @@ class OAuth2Server { * Retrieves a new token for an authorized token request. * **Remarks:** * If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. - * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. + * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. Note: setting a grant to `false` disables only the *presence* check of the `client_secret` for that grant, and does so for **all** clients (not just public ones); it does not validate a secret that is sent. The same applies to `authorization_code` requests that use PKCE (a `code_verifier`), where the presence check is skipped. Per-client (public vs confidential) authentication must therefore be enforced in your `Model#getClient() ` implementation, which should reject a confidential client that fails to present its `client_secret`. * ```js * let options = { * // ...