diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fa901fd8..25129133 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -70,7 +70,7 @@ From here you can run several scripts for development purposes:
```bash
npm run test # runs the tests once
npm run test:coverage # runs the tests including coverage
-npm run docs # generates the API docs
+npm run docs:api # generates the API docs
```
To work on a new feature or a fix please create a new branch:
diff --git a/README.md b/README.md
index 93599574..2615d057 100644
--- a/README.md
+++ b/README.md
@@ -1,79 +1,125 @@
-
# @node-oauth/oauth2-server
-Complete, compliant and well tested module for implementing an OAuth2 server in [Node.js](https://nodejs.org).
-
-[](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml)
-[](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml)
-[](https://node-oauthoauth2-server.readthedocs.io/en/latest/?badge=latest)
[](https://www.repostatus.org/#active)
[](https://www.npmjs.com/package/@node-oauth/oauth2-server)
[](https://www.npmjs.com/package/@node-oauth/oauth2-server)
[](https://github.com/node-oauth/node-oauth2-server/blob/master/LICENSE)
+[](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml)
[](https://biomejs.dev/)
+[](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml)
+[](https://github.com/node-oauth/node-oauth2-server/actions/workflows/deploy_docs.yml)
+
+A complete, compliant, and well-tested module for implementing an OAuth 2.0 server in [Node.js](https://nodejs.org).
+This project is a continuation of the
+abandoned [oauthjs/node-oauth2-server](https://github.com/oauthjs/node-oauth2-server) project.
+
+> [!NOTE]
+> ๐ก๏ธ If you intend to submit a security issue or vulnerability, please read
+> our [security policy](https://github.com/node-oauth/node-oauth2-server/security/policy) first.
+
+## Table of Contents
+
+
+
+
+- [Features](#features)
+- [Installation and Usage](#installation-and-usage)
+ - [Express and Koa Users](#express-and-koa-users)
+ - [Installation](#installation)
+ - [Usage Guide](#usage-guide)
+ - [Examples](#examples)
+- [Migration Notes](#migration-notes)
+ - [Version 5 Notes](#version-5-notes)
+ - [Migrating from oauthjs/node-oauth2-server and 3.x](#migrating-from-oauthjsnode-oauth2-server-and-3x)
+ - [Supported Node.js Versions](#supported-nodejs-versions)
+- [Contributing to This Project](#contributing-to-this-project)
+ - [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities)
+ - [Development, Issues, and Pull Requests](#development-issues-and-pull-requests)
+- [License](#license)
+
+
+
+## Features
-NOTE: This project has been forked from [oauthjs/node-oauth2-server](https://github.com/oauthjs/node-oauth2-server) and is a continuation due to the project appearing to be abandoned. Please see [our issue board](https://github.com/node-oauth/node-oauth2-server/issues) to talk about next steps and the future of this project.
+- Supports the `authorization_code`, `client_credentials`, `refresh_token`, and `password` grants, as well as *extension
+ grants*, with scopes.
+- Can be used with *promises*, *ES6 generators*, and *async*/*await*.
+- Fully [RFC 6749](https://tools.ietf.org/html/rfc6749.html) and [RFC 6750](https://tools.ietf.org/html/rfc6750.html)
+ compliant.
+- Storage-agnostic: supports any form of storage, including *PostgreSQL*, *MySQL*, *MongoDB*, and *Redis*.
+- Supports [PKCE](https://node-oauth.github.io/node-oauth2-server/guide/pkce.html).
+- Complete [test suite](https://github.com/node-oauth/node-oauth2-server/tree/master/test).
-## Installation
+## Installation and Usage
+
+### Express and Koa Users
+
+The `@node-oauth/oauth2-server` module is framework-agnostic.
+Most users should refer to our [Express (active, maintained by us)](https://github.com/node-oauth/express-oauth-server)
+or
+[Koa (not maintained by us)](https://github.com/oauthjs/koa-oauth-server/tree/master/examples) adapters.
+These adapters integrate the library with their respective frameworks.
+
+> [!TIP]
+> If you're using one of these frameworks, we strongly recommend using the corresponding wrapper module instead of
+> writing your own integration.
+
+### Installation
+
+The base installation is intended for users who want to fully customize the integration
+of this library into their stack.
```bash
npm install @node-oauth/oauth2-server
```
-The `@node-oauth/oauth2-server` module is framework-agnostic but there are several officially supported wrappers available for popular HTTP server frameworks such as [Express](https://www.npmjs.com/package/@node-oauth/express-oauth-server) and [Koa (not maintained by us)](https://npmjs.org/package/koa-oauth-server).
-If you're using one of those frameworks it is strongly recommended to use the respective wrapper module instead of rolling your own.
-
+### Usage Guide
-## Features
+Documentation is available at https://node-oauth.github.io/node-oauth2-server/.
-- Supports `authorization_code`, `client_credentials`, `refresh_token` and `password` grant, as well as *extension grants*, with scopes.
-- Can be used with *promises*, *ES6 generators* and *async*/*await* (using [Babel](https://babeljs.io)).
-- Fully [RFC 6749](https://tools.ietf.org/html/rfc6749.html) and [RFC 6750](https://tools.ietf.org/html/rfc6750.html) compliant.
-- Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc.
-- Support for PKCE
-- Complete [test suite](https://github.com/node-oauth/node-oauth2-server/tree/master/test).
+Please open an issue if something is confusing or missing in the documentation.
-## Documentation
+### Examples
-Documentation is hosted on Read the Docs. We have multiple versions of the docs available:
+More examples can be found in our examples repository: https://github.com/node-oauth/node-oauth2-server-examples
-- [stable](https://node-oauthoauth2-server.readthedocs.io/en/master/) (master branch)
-- [development](https://node-oauthoauth2-server.readthedocs.io/en/development/) (development branch)
+## Migration Notes
-Please leave an issue if something is confusing or missing in the docs.
+### Version 5 Notes
-## Examples
+Starting with version `5.x`, we removed support for callbacks.
+This version supports only promises and async/await.
-Most users should refer to our [Express (active)](https://github.com/node-oauth/express-oauth-server) or
-[Koa (not maintained by us)](https://github.com/oauthjs/koa-oauth-server/tree/master/examples) examples.
+In this version, we also raised the minimum required Node.js version to 16, as Node.js 14 was deprecated.
-More examples can be found here: https://github.com/14gasher/oauth-example
+### Migrating from [oauthjs/node-oauth2-server](https://github.com/oauthjs/node-oauth2-server) and 3.x
-## Version 5 notes
+Version 4.x should not introduce major breaking changes. However, it includes many improvements and fixes that may
+be incompatible with specific behavior in version 3.x and earlier.
-Beginning with version `5.x` we removed dual support for callbacks and promises.
-With this version there is only support for Promises / async/await.
+### Supported Node.js Versions
-With this version we also bumped the `engine` to Node 16 as 14 is now deprecated.
+We aim to maintain compatibility with older Node.js versions wherever possible.
+If our minimum supported Node.js version predates the currently supported or LTS releases, this reflects our
+commitment to backward compatibility and does not mean that this library is deprecated.
+This approach lets us provide a stable OAuth 2.0 server for legacy systems with the same features available to modern
+systems.
-## Migrating from OAuthJs and 3.x
+## Contributing to This Project
-Version 4.x should not be hard-breaking, however, there were many improvements and fixes that may
-be incompatible with specific behaviour in <= 3.x
+### Reporting Security Vulnerabilities
-For more info, please read the [changelog](./CHANGELOG.md) or open an issue, if you think something
-is unexpectedly not working.
+If you intend to submit a security issue or vulnerability, please read
+our [security policy](https://github.com/node-oauth/node-oauth2-server/security/policy) first.
-## Supported NodeJs versions
+The policy is also available locally in [SECURITY.md](./SECURITY.md).
-This project supports the node versions along the
-[NodeJS LTS releases](https://nodejs.org/en/about/releases/), focusing on
+### Development, Issues, and Pull Requests
-- Maintenance LTS
-- Active LTS
-- Current
+Please read our [contribution guide](./CONTRIBUTING.md) before contributing.
+Please open an issue before submitting a pull request to discuss whether your proposed contribution is suitable for
+the project.
-## Contributing to this project
+## License
-Please read our [contribution guide](./CONTRIBUTING.md) before taking actions.
-In any case, please open an issue before opening a pull request to find out whether your intended contribution will actually have a chance to be merged.
+This library is licensed under the MIT License. See the [LICENSE file](./LICENSE).
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index fce00187..75bb4d80 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -2,6 +2,26 @@ import {defineConfig} from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
+ srcExclude: ['plans/**'],
+ markdown: {
+ config(md) {
+ // VitePress normalizes Markdown fragments, but JSDoc emits raw named
+ // anchors. Add matching IDs while preserving the original names.
+ for (const rule of ['html_inline', 'html_block']) {
+ const render = md.renderer.rules[rule]!
+ md.renderer.rules[rule] = (tokens, index, options, env, self) => {
+ return render(tokens, index, options, env, self).replace(
+ //g,
+ (anchor, name: string) => {
+ if (!/[_.+]/.test(name)) return anchor
+ const id = name.replace(/[_.+]+/g, '-').toLowerCase()
+ return ``
+ }
+ )
+ }
+ }
+ }
+ },
title: "@node-oauth/oauth2-server",
description: "OAuth2 server for Node.js",
base: '/node-oauth2-server/',
diff --git a/docs/api/errors/invalid-argument-error.md b/docs/api/errors/invalid-argument-error.md
index a5d8649b..85a1136e 100644
--- a/docs/api/errors/invalid-argument-error.md
+++ b/docs/api/errors/invalid-argument-error.md
@@ -15,12 +15,12 @@ depends on *when* it is raised:
a required method, or `request`/`response` not being instances of this
library's `Request`/`Response`) it is thrown to your code unchanged, before
any response is produced โ so you can catch it while wiring up the server.
-- **While handling a request** (for example, when a `model` returns malformed
- token data) the `token` and `authorize` handlers normalise it to a
- `ServerError` (`server_error`) before the response reaches the client,
- preserving the original error as `.inner`. So when you `catch` errors from
- `OAuth2Server#token` or `OAuth2Server#authorize`, expect a `ServerError`
- (inspect its `.inner`) for these cases โ not an `InvalidArgumentError`.
+- **Inside the token/authorize normalization blocks**, programming and non-OAuth
+ errors become `ServerError` with the original error in `.inner`.
+- **Before those blocks**, errors propagate unchanged. In particular, authorization
+ client and user lookup run before its catch, as do token method/content-type
+ checks. Authentication preserves OAuth errors, including `InvalidArgumentError`.
+Integrations must handle early failures locally and avoid exposing internal errors.
**Kind**: global class
diff --git a/docs/api/grant-types/abstract-grant-type.md b/docs/api/grant-types/abstract-grant-type.md
index 081c97e3..dc69dac5 100644
--- a/docs/api/grant-types/abstract-grant-type.md
+++ b/docs/api/grant-types/abstract-grant-type.md
@@ -7,11 +7,11 @@
* [AbstractGrantType](#AbstractGrantType)
* [new AbstractGrantType(options)](#new_AbstractGrantType_new)
* [.generateAccessToken(client, user, scope)](#AbstractGrantType+generateAccessToken) โ Promise.<string>
- * [.generateRefreshToken()](#AbstractGrantType+generateRefreshToken)
+ * [.generateRefreshToken(client, user, scope)](#AbstractGrantType+generateRefreshToken)
* [.getAccessTokenExpiresAt()](#AbstractGrantType+getAccessTokenExpiresAt)
* [.getRefreshTokenExpiresAt()](#AbstractGrantType+getRefreshTokenExpiresAt) โ Date
- * [.getScope(request)](#AbstractGrantType+getScope) โ string \| undefined
- * [.validateScope(user, client, scope)](#AbstractGrantType+validateScope) โ string
+ * [.getScope(request)](#AbstractGrantType+getScope) โ Array.<string> \| undefined
+ * [.validateScope(user, client, scope)](#AbstractGrantType+validateScope) โ Promise.<(Array.<string>\|undefined)>
@@ -40,18 +40,25 @@ Otherwise, falls back to an internal implementation from `TokenUtil.generateRand
**Kind**: instance method of [AbstractGrantType](#AbstractGrantType)
-| Param |
-| --- |
-| client |
-| user |
-| scope |
+| Param | Type |
+| --- | --- |
+| client | ClientData |
+| user | object |
+| scope | Array.<string> \| undefined |
-### abstractGrantType.generateRefreshToken()
+### abstractGrantType.generateRefreshToken(client, user, scope)
Generate refresh token.
**Kind**: instance method of [AbstractGrantType](#AbstractGrantType)
+
+| Param | Type |
+| --- | --- |
+| client | ClientData |
+| user | object |
+| scope | Array.<string> \| undefined |
+
### abstractGrantType.getAccessTokenExpiresAt()
@@ -66,7 +73,7 @@ Get refresh token expiration date (now + refresh token lifetime)
**Kind**: instance method of [AbstractGrantType](#AbstractGrantType)
-### abstractGrantType.getScope(request) โ string \| undefined
+### abstractGrantType.getScope(request) โ Array.<string> \| undefined
Get scope from the request body.
**Kind**: instance method of [AbstractGrantType](#AbstractGrantType)
@@ -77,14 +84,14 @@ Get scope from the request body.
-### abstractGrantType.validateScope(user, client, scope) โ string
+### abstractGrantType.validateScope(user, client, scope) โ Promise.<(Array.<string>\|undefined)>
Validate requested scope.
Delegates validation to the Model's `validateScope` method,
if the model implements this method.
Otherwise, treats given scope as valid.
**Kind**: instance method of [AbstractGrantType](#AbstractGrantType)
-**Returns**: string - the validated scope
+**Returns**: Promise.<(Array.<string>\|undefined)> - the validated scope
**Throws**:
- InvalidScopeError if the {Model#validateScope} method returned a falsy value
@@ -94,5 +101,5 @@ Otherwise, treats given scope as valid.
| --- | --- |
| user | object |
| client | ClientData |
-| scope | string |
+| scope | Array.<string> \| undefined |
diff --git a/docs/api/grant-types/authorization-code-grant-type.md b/docs/api/grant-types/authorization-code-grant-type.md
index 18a58c4d..4283fb16 100644
--- a/docs/api/grant-types/authorization-code-grant-type.md
+++ b/docs/api/grant-types/authorization-code-grant-type.md
@@ -6,7 +6,7 @@
* [AuthorizationCodeGrantType](#AuthorizationCodeGrantType)
* [new AuthorizationCodeGrantType(options)](#new_AuthorizationCodeGrantType_new)
* [.handle(request, client)](#AuthorizationCodeGrantType+handle)
- * [.getAuthorizationCode(request, client)](#AuthorizationCodeGrantType+getAuthorizationCode) โ Promise.<{user}>
+ * [.getAuthorizationCode(request, client)](#AuthorizationCodeGrantType+getAuthorizationCode) โ Promise.<AuthorizationCodeData>
* [.verifyPKCE(request, code)](#AuthorizationCodeGrantType+verifyPKCE)
* [.validateRedirectUri(request, code)](#AuthorizationCodeGrantType+validateRedirectUri)
* [.revokeAuthorizationCode(code)](#AuthorizationCodeGrantType+revokeAuthorizationCode)
@@ -35,7 +35,7 @@ Handle authorization code grant.
-### authorizationCodeGrantType.getAuthorizationCode(request, client) โ Promise.<{user}>
+### authorizationCodeGrantType.getAuthorizationCode(request, client) โ Promise.<AuthorizationCodeData>
Get the authorization code.
**Kind**: instance method of [AuthorizationCodeGrantType](#AuthorizationCodeGrantType)
@@ -108,5 +108,5 @@ Save token.
| user | object |
| client | ClientData |
| authorizationCode | string |
-| requestedScope | string |
+| requestedScope | Array.<string> \| undefined |
diff --git a/docs/api/grant-types/client-credentials-grant-type.md b/docs/api/grant-types/client-credentials-grant-type.md
index e31aa52b..c2edc133 100644
--- a/docs/api/grant-types/client-credentials-grant-type.md
+++ b/docs/api/grant-types/client-credentials-grant-type.md
@@ -6,26 +6,44 @@ Constructor.
**Kind**: global class
* [ClientCredentialsGrantType](#ClientCredentialsGrantType)
- * [.handle()](#ClientCredentialsGrantType+handle)
- * [.getUserFromClient()](#ClientCredentialsGrantType+getUserFromClient)
- * [.saveToken()](#ClientCredentialsGrantType+saveToken)
+ * [.handle(request, client)](#ClientCredentialsGrantType+handle)
+ * [.getUserFromClient(client)](#ClientCredentialsGrantType+getUserFromClient)
+ * [.saveToken(user, client, requestedScope)](#ClientCredentialsGrantType+saveToken)
-### clientCredentialsGrantType.handle()
+### clientCredentialsGrantType.handle(request, client)
Handle client credentials grant.
**Kind**: instance method of [ClientCredentialsGrantType](#ClientCredentialsGrantType)
**See**: https://tools.ietf.org/html/rfc6749#section-4.4.2
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### clientCredentialsGrantType.getUserFromClient()
+### clientCredentialsGrantType.getUserFromClient(client)
Retrieve the user using client credentials.
**Kind**: instance method of [ClientCredentialsGrantType](#ClientCredentialsGrantType)
+
+| Param | Type |
+| --- | --- |
+| client | ClientData |
+
-### clientCredentialsGrantType.saveToken()
+### clientCredentialsGrantType.saveToken(user, client, requestedScope)
Save token.
**Kind**: instance method of [ClientCredentialsGrantType](#ClientCredentialsGrantType)
+
+| Param | Type |
+| --- | --- |
+| user | object |
+| client | ClientData |
+| requestedScope | Array.<string> \| undefined |
+
diff --git a/docs/api/grant-types/password-grant-type.md b/docs/api/grant-types/password-grant-type.md
index c3f13452..d9f1c6d0 100644
--- a/docs/api/grant-types/password-grant-type.md
+++ b/docs/api/grant-types/password-grant-type.md
@@ -8,26 +8,45 @@ Constructor.
**Kind**: global class
* ~~[PasswordGrantType](#PasswordGrantType)~~
- * [.handle()](#PasswordGrantType+handle)
- * [.getUser()](#PasswordGrantType+getUser)
- * [.saveToken()](#PasswordGrantType+saveToken)
+ * [.handle(request, client)](#PasswordGrantType+handle)
+ * [.getUser(request, client)](#PasswordGrantType+getUser)
+ * [.saveToken(user, client, requestedScope)](#PasswordGrantType+saveToken)
-### passwordGrantType.handle()
+### passwordGrantType.handle(request, client)
Retrieve the user from the model using a username/password combination.
**Kind**: instance method of [PasswordGrantType](#PasswordGrantType)
**See**: https://tools.ietf.org/html/rfc6749#section-4.3.2
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### passwordGrantType.getUser()
+### passwordGrantType.getUser(request, client)
Get user using a username/password combination.
**Kind**: instance method of [PasswordGrantType](#PasswordGrantType)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### passwordGrantType.saveToken()
+### passwordGrantType.saveToken(user, client, requestedScope)
Save token.
**Kind**: instance method of [PasswordGrantType](#PasswordGrantType)
+
+| Param | Type |
+| --- | --- |
+| user | object |
+| client | ClientData |
+| requestedScope | Array.<string> \| undefined |
+
diff --git a/docs/api/grant-types/refresh-token-grant-type.md b/docs/api/grant-types/refresh-token-grant-type.md
index 5ccece94..2e36b76b 100644
--- a/docs/api/grant-types/refresh-token-grant-type.md
+++ b/docs/api/grant-types/refresh-token-grant-type.md
@@ -6,34 +6,58 @@ Constructor.
**Kind**: global class
* [RefreshTokenGrantType](#RefreshTokenGrantType)
- * [.handle()](#RefreshTokenGrantType+handle)
- * [.getRefreshToken()](#RefreshTokenGrantType+getRefreshToken)
- * [.revokeToken()](#RefreshTokenGrantType+revokeToken)
- * [.saveToken()](#RefreshTokenGrantType+saveToken)
+ * [.handle(request, client)](#RefreshTokenGrantType+handle)
+ * [.getRefreshToken(request, client)](#RefreshTokenGrantType+getRefreshToken)
+ * [.revokeToken(token)](#RefreshTokenGrantType+revokeToken)
+ * [.saveToken(user, client, scope)](#RefreshTokenGrantType+saveToken)
-### refreshTokenGrantType.handle()
+### refreshTokenGrantType.handle(request, client)
Handle refresh token grant.
**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType)
**See**: https://tools.ietf.org/html/rfc6749#section-6
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### refreshTokenGrantType.getRefreshToken()
+### refreshTokenGrantType.getRefreshToken(request, client)
Get refresh token.
**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### refreshTokenGrantType.revokeToken()
+### refreshTokenGrantType.revokeToken(token)
Revoke the refresh token.
**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType)
**See**: https://tools.ietf.org/html/rfc6749#section-6
+
+| Param | Type |
+| --- | --- |
+| token | object |
+
-### refreshTokenGrantType.saveToken()
+### refreshTokenGrantType.saveToken(user, client, scope)
Save token.
**Kind**: instance method of [RefreshTokenGrantType](#RefreshTokenGrantType)
+
+| Param | Type |
+| --- | --- |
+| user | object |
+| client | ClientData |
+| scope | Array.<string> \| undefined |
+
diff --git a/docs/api/handlers/authenticate-handler.md b/docs/api/handlers/authenticate-handler.md
index 243c1ef9..8dde985e 100644
--- a/docs/api/handlers/authenticate-handler.md
+++ b/docs/api/handlers/authenticate-handler.md
@@ -11,9 +11,9 @@
* [.getTokenFromRequestQuery(request)](#AuthenticateHandler+getTokenFromRequestQuery)
* [.getTokenFromRequestBody(request)](#AuthenticateHandler+getTokenFromRequestBody)
* [.getAccessToken(token)](#AuthenticateHandler+getAccessToken)
- * [.validateAccessToken()](#AuthenticateHandler+validateAccessToken)
- * [.verifyScope()](#AuthenticateHandler+verifyScope)
- * [.updateResponse()](#AuthenticateHandler+updateResponse)
+ * [.validateAccessToken(accessToken)](#AuthenticateHandler+validateAccessToken)
+ * [.verifyScope(accessToken)](#AuthenticateHandler+verifyScope)
+ * [.updateResponse(response, accessToken)](#AuthenticateHandler+updateResponse)
@@ -27,7 +27,7 @@
| --- | --- | --- | --- |
| options | object | | Server options. |
| options.model | Model | | The Model; this is always required. |
-| options.scope | Array.<string> \| undefined | | The scope(s) to authenticate. |
+| options.scope | string \| Array.<string> \| undefined | | The scope(s) to authenticate. |
| [options.addAcceptedScopesHeader] | boolean | true | Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. |
| [options.addAuthorizedScopesHeader] | boolean | true | Set the `X-OAuth-Scopes` HTTP header on response objects. |
| [options.allowBearerTokensInQueryString] | boolean | false | Allow clients to pass bearer tokens in the query string of a request. |
@@ -118,19 +118,35 @@ Get the access token from the model.
-### authenticateHandler.validateAccessToken()
+### authenticateHandler.validateAccessToken(accessToken)
Validate access token.
**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler)
+
+| Param | Type |
+| --- | --- |
+| accessToken | AccessTokenData |
+
-### authenticateHandler.verifyScope()
+### authenticateHandler.verifyScope(accessToken)
Verify scope.
**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler)
+
+| Param | Type |
+| --- | --- |
+| accessToken | AccessTokenData |
+
-### authenticateHandler.updateResponse()
+### authenticateHandler.updateResponse(response, accessToken)
Update response.
**Kind**: instance method of [AuthenticateHandler](#AuthenticateHandler)
+
+| Param | Type |
+| --- | --- |
+| response | Response |
+| accessToken | AccessTokenData |
+
diff --git a/docs/api/handlers/authorize-handler.md b/docs/api/handlers/authorize-handler.md
index a2fca39b..83656fe5 100644
--- a/docs/api/handlers/authorize-handler.md
+++ b/docs/api/handlers/authorize-handler.md
@@ -22,34 +22,47 @@ Constructor.
**Kind**: global class
* [AuthorizeHandler](#AuthorizeHandler)
- * [.handle()](#AuthorizeHandler+handle)
- * [.generateAuthorizationCode()](#AuthorizeHandler+generateAuthorizationCode)
+ * [.handle(request, response)](#AuthorizeHandler+handle)
+ * [.generateAuthorizationCode(client, user, scope)](#AuthorizeHandler+generateAuthorizationCode)
* [.getAuthorizationCodeLifetime()](#AuthorizeHandler+getAuthorizationCodeLifetime)
- * [.getClient()](#AuthorizeHandler+getClient)
- * [.validateScope()](#AuthorizeHandler+validateScope)
- * [.getScope()](#AuthorizeHandler+getScope)
- * [.getState()](#AuthorizeHandler+getState)
- * [.getUser()](#AuthorizeHandler+getUser)
- * [.getRedirectUri()](#AuthorizeHandler+getRedirectUri)
- * [.saveAuthorizationCode()](#AuthorizeHandler+saveAuthorizationCode)
- * [.getResponseType()](#AuthorizeHandler+getResponseType)
- * [.buildSuccessRedirectUri()](#AuthorizeHandler+buildSuccessRedirectUri)
- * [.buildErrorRedirectUri()](#AuthorizeHandler+buildErrorRedirectUri)
- * [.updateResponse()](#AuthorizeHandler+updateResponse)
- * [.getCodeChallengeMethod()](#AuthorizeHandler+getCodeChallengeMethod)
+ * [.getClient(request)](#AuthorizeHandler+getClient)
+ * [.validateScope(user, client, scope)](#AuthorizeHandler+validateScope) โ Promise.<(Array.<string>\|undefined)>
+ * [.getScope(request)](#AuthorizeHandler+getScope) โ Array.<string> \| undefined
+ * [.getState(request)](#AuthorizeHandler+getState)
+ * [.getUser(request, response)](#AuthorizeHandler+getUser)
+ * [.getRedirectUri(request, client)](#AuthorizeHandler+getRedirectUri)
+ * [.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod)](#AuthorizeHandler+saveAuthorizationCode)
+ * [.getResponseType(request)](#AuthorizeHandler+getResponseType)
+ * [.buildSuccessRedirectUri(redirectUri, responseType)](#AuthorizeHandler+buildSuccessRedirectUri)
+ * [.buildErrorRedirectUri(redirectUri, error)](#AuthorizeHandler+buildErrorRedirectUri)
+ * [.updateResponse(response, redirectUri, state)](#AuthorizeHandler+updateResponse)
+ * [.getCodeChallengeMethod(request)](#AuthorizeHandler+getCodeChallengeMethod)
-### authorizeHandler.handle()
+### authorizeHandler.handle(request, response)
Authorize Handler.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| response | Response |
+
-### authorizeHandler.generateAuthorizationCode()
+### authorizeHandler.generateAuthorizationCode(client, user, scope)
Generate authorization code.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| client | ClientData |
+| user | object |
+| scope | Array.<string> \| undefined |
+
### authorizeHandler.getAuthorizationCodeLifetime()
@@ -58,73 +71,144 @@ Get authorization code lifetime.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
-### authorizeHandler.getClient()
+### authorizeHandler.getClient(request)
Get the client from the model.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+
-### authorizeHandler.validateScope()
+### authorizeHandler.validateScope(user, client, scope) โ Promise.<(Array.<string>\|undefined)>
Validate requested scope.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+**Returns**: Promise.<(Array.<string>\|undefined)> - The accepted scopes.
+
+| Param | Type |
+| --- | --- |
+| user | object |
+| client | ClientData |
+| scope | Array.<string> \| undefined |
+
-### authorizeHandler.getScope()
+### authorizeHandler.getScope(request) โ Array.<string> \| undefined
Get scope from the request.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+
-### authorizeHandler.getState()
+### authorizeHandler.getState(request)
Get state from the request.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+
-### authorizeHandler.getUser()
+### authorizeHandler.getUser(request, response)
Get user by calling the authenticate middleware.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| response | Response |
+
-### authorizeHandler.getRedirectUri()
+### authorizeHandler.getRedirectUri(request, client)
Get redirect URI.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### authorizeHandler.saveAuthorizationCode()
+### authorizeHandler.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod)
Save authorization code.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| authorizationCode | string |
+| expiresAt | Date |
+| scope | Array.<string> \| undefined |
+| client | ClientData |
+| redirectUri | string |
+| user | object |
+| codeChallenge | string \| undefined |
+| codeChallengeMethod | string \| undefined |
+
-### authorizeHandler.getResponseType()
+### authorizeHandler.getResponseType(request)
Get response type.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+
-### authorizeHandler.buildSuccessRedirectUri()
+### authorizeHandler.buildSuccessRedirectUri(redirectUri, responseType)
Build a successful response that redirects the user-agent to the client-provided url.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| redirectUri | string |
+| responseType | object |
+
-### authorizeHandler.buildErrorRedirectUri()
+### authorizeHandler.buildErrorRedirectUri(redirectUri, error)
Build an error response that redirects the user-agent to the client-provided url.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type |
+| --- | --- |
+| redirectUri | string |
+| error | Error |
+
-### authorizeHandler.updateResponse()
+### authorizeHandler.updateResponse(response, redirectUri, state)
Update response with the redirect uri and the state parameter, if available.
**Kind**: instance method of [AuthorizeHandler](#AuthorizeHandler)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| response | Response | |
+| redirectUri | object | Parsed URL returned by the redirect builders. |
+| state | string \| undefined | |
+
-### authorizeHandler.getCodeChallengeMethod()
+### authorizeHandler.getCodeChallengeMethod(request)
Get code challenge method from request.
When `enablePlainPKCE` is false (the default), the "plain" method is
@@ -139,6 +223,11 @@ default per RFC 7636 ยง4.3.
(see https://www.rfc-editor.org/rfc/rfc7636#section-4.4)
**See**: https://www.rfc-editor.org/rfc/rfc7636#section-4.3
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+
## responseTypes
diff --git a/docs/api/handlers/token-handler.md b/docs/api/handlers/token-handler.md
index d0817a0d..bdd8df1e 100644
--- a/docs/api/handlers/token-handler.md
+++ b/docs/api/handlers/token-handler.md
@@ -22,32 +22,44 @@ Constructor.
**Kind**: global class
* [TokenHandler](#TokenHandler)
- * [.handle()](#TokenHandler+handle)
- * [.getClient()](#TokenHandler+getClient)
- * [.getClientCredentials()](#TokenHandler+getClientCredentials)
- * [.handleGrantType()](#TokenHandler+handleGrantType)
- * [.getAccessTokenLifetime()](#TokenHandler+getAccessTokenLifetime)
- * [.getRefreshTokenLifetime()](#TokenHandler+getRefreshTokenLifetime)
- * [.getTokenType()](#TokenHandler+getTokenType)
- * [.updateSuccessResponse()](#TokenHandler+updateSuccessResponse)
- * [.updateErrorResponse()](#TokenHandler+updateErrorResponse)
- * [.isClientAuthenticationRequired()](#TokenHandler+isClientAuthenticationRequired)
+ * [.handle(request, response)](#TokenHandler+handle)
+ * [.getClient(request, response)](#TokenHandler+getClient)
+ * [.getClientCredentials(request)](#TokenHandler+getClientCredentials)
+ * [.handleGrantType(request, client)](#TokenHandler+handleGrantType)
+ * [.getAccessTokenLifetime(client)](#TokenHandler+getAccessTokenLifetime)
+ * [.getRefreshTokenLifetime(client)](#TokenHandler+getRefreshTokenLifetime)
+ * [.getTokenType(model)](#TokenHandler+getTokenType)
+ * [.updateSuccessResponse(response, tokenType)](#TokenHandler+updateSuccessResponse)
+ * [.updateErrorResponse(response, error)](#TokenHandler+updateErrorResponse)
+ * [.isClientAuthenticationRequired(grantType)](#TokenHandler+isClientAuthenticationRequired)
-### tokenHandler.handle()
+### tokenHandler.handle(request, response)
Token Handler.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| response | Response |
+
-### tokenHandler.getClient()
+### tokenHandler.getClient(request, response)
Get the client from the model.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| response | Response |
+
-### tokenHandler.getClientCredentials()
+### tokenHandler.getClientCredentials(request)
Get client credentials.
The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively,
@@ -55,48 +67,91 @@ the `client_id` and `client_secret` can be embedded in the body.
**Kind**: instance method of [TokenHandler](#TokenHandler)
**See**: https://tools.ietf.org/html/rfc6749#section-2.3.1
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+
-### tokenHandler.handleGrantType()
+### tokenHandler.handleGrantType(request, client)
Handle grant type.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| request | Request |
+| client | ClientData |
+
-### tokenHandler.getAccessTokenLifetime()
+### tokenHandler.getAccessTokenLifetime(client)
Get access token lifetime.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| client | ClientData |
+
-### tokenHandler.getRefreshTokenLifetime()
+### tokenHandler.getRefreshTokenLifetime(client)
Get refresh token lifetime.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| client | ClientData |
+
-### tokenHandler.getTokenType()
+### tokenHandler.getTokenType(model)
Get token type.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| model | TokenModel |
+
-### tokenHandler.updateSuccessResponse()
+### tokenHandler.updateSuccessResponse(response, tokenType)
Update response when a token is generated.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| response | Response |
+| tokenType | BearerTokenType |
+
-### tokenHandler.updateErrorResponse()
+### tokenHandler.updateErrorResponse(response, error)
Update response when an error is thrown.
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| response | Response |
+| error | Error |
+
-### tokenHandler.isClientAuthenticationRequired()
+### tokenHandler.isClientAuthenticationRequired(grantType)
Given a grant type, check if client authentication is required
**Kind**: instance method of [TokenHandler](#TokenHandler)
+
+| Param | Type |
+| --- | --- |
+| grantType | string |
+
## grantTypes
diff --git a/docs/api/model.md b/docs/api/model.md
index c5d420d8..909f590e 100644
--- a/docs/api/model.md
+++ b/docs/api/model.md
@@ -7,7 +7,10 @@ which some aspects of storage, retrieval and custom
validation are abstracted.
Each model function is resolved async by default.
This implies that async and sync model functions,
-as well as generators, are supported.
+returning values or native Promises are supported. Direct generator hooks are not run.
+Promise return types below also permit the corresponding synchronous value.
+This class is an internal reference, not exported by the package entry point;
+applications normally pass a plain object of hooks.
@@ -37,7 +40,10 @@ validation are abstracted.
Each model function is resolved async by default.
This implies that async and sync model functions,
-as well as generators, are supported.
+returning values or native Promises are supported. Direct generator hooks are not run.
+Promise return types below also permit the corresponding synchronous value.
+This class is an internal reference, not exported by the package entry point;
+applications normally pass a plain object of hooks.
**Kind**: global class
@@ -58,7 +64,7 @@ as well as generators, are supported.
* [.generateAccessToken(client, user, scope)](#Model+generateAccessToken) โ Promise.<string>
* [.generateRefreshToken(client, user, scope)](#Model+generateRefreshToken) โ Promise.<string>
* [.generateAuthorizationCode(client, user, scope)](#Model+generateAuthorizationCode) โ Promise.<string>
- * [.validateScope(user, client, scope)](#Model+validateScope) โ Promise.<boolean>
+ * [.validateScope(user, client, [scope])](#Model+validateScope) โ Promise.<(Array.<string>\|null\|undefined\|false\|0)>
* [.validateRedirectUri(redirectUri, client)](#Model+validateRedirectUri) โ Promise.<boolean>
* _static_
* [.from(impl)](#Model.from) โ [Model](#Model)
@@ -68,8 +74,10 @@ as well as generators, are supported.
### new Model()
**Example**
```js
+// Repository-internal import, not a public package export:
+const Model = require('./lib/model');
const model = Model.from({
- getClient: () => { ... }
+ getClient: async (id) => db.queryClient({ id })
})
```
@@ -85,12 +93,13 @@ This model function is **required** for all grant types.
- `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.
+Authorization always calls `getClient(clientId, null)`, including for confidential
+clients. Token requests pass the supplied secret; PKCE or a per-grant option
+can permit its absence. Always validate supplied secrets. An absent secret
+alone cannot identify the endpoint or distinguish public/confidential clients.
+Confidential token requests still require authentication: use endpoint-specific
+model wrappers as shown in the [PKCE guide](../guide/pkce.md#pkce-client-authentication-and-refresh-tokens).
+Universally rejecting missing secrets here breaks confidential-client authorization.
**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.
@@ -113,7 +122,7 @@ This model function is **required** for all grant types.
- `refresh_token` grant
- `password` grant
-If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client.
+If the `allowExtendedTokenAttributes` server option is enabled (see [OAuth2Server#token](server.md#OAuth2Server+token)) any additional attributes set on the result are copied to the token response sent to the client.
**Kind**: instance method of [Model](#Model)
**Fulfil**: {accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object} An `Object` representing the token(s) and associated data.
@@ -123,44 +132,33 @@ If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Serve
| token | object | The token(s) to be saved. |
| token.accessToken | string | The access token to be saved. |
| token.accessTokenExpiresAt | Date | The expiry time of the access token. |
-| token.refreshToken | string | The refresh token to be saved. |
-| token.refreshTokenExpiresAt | Date | The expiry time of the refresh token. |
-| token.scope | Array.<string> | The authorized scope of the token(s) |
+| [token.refreshToken] | string | The refresh token to be saved. |
+| [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. |
+| [token.scope] | Array.<string> | The authorized scope of the token(s) |
| client | [ClientData](#ClientData) | The client associated with the token(s). |
| user | object | The user associated with the token(s). |
**Example**
```js
-function saveToken(token, client, user) {
- // imaginary DB queries
- let fns = [
- db.saveAccessToken({
- access_token: token.accessToken,
- expires_at: token.accessTokenExpiresAt,
- scope: token.scope,
- client_id: client.id,
- user_id: user.id
- }),
- db.saveRefreshToken({
+async function saveToken(token, client, user) {
+ // Imaginary DB methods return native Promises. Use a transaction if needed.
+ await db.saveAccessToken({
+ access_token: token.accessToken,
+ expires_at: token.accessTokenExpiresAt,
+ scope: token.scope,
+ client_id: client.id,
+ user_id: user.id
+ });
+ if (token.refreshToken) {
+ await db.saveRefreshToken({
refresh_token: token.refreshToken,
expires_at: token.refreshTokenExpiresAt,
scope: token.scope,
client_id: client.id,
user_id: user.id
- })
- ];
- return Promise.all(fns);
- .spread(function(accessToken, refreshToken) {
- return {
- accessToken: accessToken.access_token,
- accessTokenExpiresAt: accessToken.expires_at,
- refreshToken: refreshToken.refresh_token,
- refreshTokenExpiresAt: refreshToken.expires_at,
- scope: accessToken.scope,
- client: {id: accessToken.client_id},
- user: {id: accessToken.user_id}
- };
});
+ }
+ return { ...token, client, user };
}
```
@@ -203,7 +201,7 @@ This model function is **required** if the `client_credentials` grant is used.
**Remarks:**
-`client` is the object previously obtained through `Model#getClient() `.
+`client` is the object previously obtained through [Model#getClient](model.md#Model+getClient).
**Kind**: instance method of [Model](#Model)
**Returns**: Promise.<object> - An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions.
@@ -222,8 +220,8 @@ function getUserFromClient(client) {
### model.getAccessToken(accessToken) โ [Promise.<AccessTokenData>](#AccessTokenData)
-Invoked to retrieve an existing access token, including associated data, that has previously been saved through `Model#saveToken() `.
-This model function is **required** if `OAuth2Server#authenticate() ` is used.
+Invoked to retrieve an existing access token, including associated data, that has previously been saved through [Model#saveToken](model.md#Model+saveToken).
+This model function is **required** if [OAuth2Server#authenticate](server.md#OAuth2Server+authenticate) is used.
**Invoked during:**
- request authentication
@@ -241,13 +239,15 @@ function getAccessToken(accessToken) {
// imaginary DB queries
return db.queryAccessToken({access_token: accessToken})
.then(function(token) {
+ if (!token) return [null];
return Promise.all([
token,
db.queryClient({id: token.client_id}),
db.queryUser({id: token.user_id})
]);
})
- .spread(function(token, client, user) {
+ .then(function([token, client, user]) {
+ if (!token) return false;
return {
accessToken: token.access_token,
accessTokenExpiresAt: token.expires_at,
@@ -261,7 +261,7 @@ function getAccessToken(accessToken) {
### model.getRefreshToken(refreshToken) โ [Promise.<RefreshTokenData>](#RefreshTokenData)
-Invoked to retrieve an existing refresh token previously saved through `Model#saveToken() `.
+Invoked to retrieve an existing refresh token previously saved through [Model#saveToken](model.md#Model+saveToken).
This model function is **required** if the `refresh_token` grant is used.
**Invoked during:**
- `refresh_token` grant
@@ -271,7 +271,7 @@ This model function is **required** if the `refresh_token` grant is used.
| Param | Type | Description |
| --- | --- | --- |
-| refreshToken | string | The access token to retrieve. |
+| refreshToken | string | The refresh token to retrieve. |
**Example**
```js
@@ -279,13 +279,15 @@ function getRefreshToken(refreshToken) {
// imaginary DB queries
return db.queryRefreshToken({refresh_token: refreshToken})
.then(function(token) {
+ if (!token) return [null];
return Promise.all([
token,
db.queryClient({id: token.client_id}),
db.queryUser({id: token.user_id})
]);
})
- .spread(function(token, client, user) {
+ .then(function([token, client, user]) {
+ if (!token) return false;
return {
refreshToken: token.refresh_token,
refreshTokenExpiresAt: token.expires_at,
@@ -299,7 +301,7 @@ function getRefreshToken(refreshToken) {
### model.getAuthorizationCode(authorizationCode) โ [Promise.<AuthorizationCodeData>](#AuthorizationCodeData)
-Invoked to retrieve an existing authorization code previously saved through `Model#saveAuthorizationCode() `.
+Invoked to retrieve an existing authorization code previously saved through [Model#saveAuthorizationCode](model.md#Model+saveAuthorizationCode).
This model function is **required** if the `authorization_code` grant is used.
**Invoked during:**
- `authorization_code` grant
@@ -317,17 +319,21 @@ function getAuthorizationCode(authorizationCode) {
// imaginary DB queries
return db.queryAuthorizationCode({authorization_code: authorizationCode})
.then(function(code) {
+ if (!code) return [null];
return Promise.all([
code,
db.queryClient({id: code.client_id}),
db.queryUser({id: code.user_id})
]);
})
- .spread(function(code, client, user) {
+ .then(function([code, client, user]) {
+ if (!code) return false;
return {
authorizationCode: code.authorization_code,
expiresAt: code.expires_at,
redirectUri: code.redirect_uri,
+ codeChallenge: code.code_challenge,
+ codeChallengeMethod: code.code_challenge_method,
scope: code.scope,
client: client, // with 'id' property
user: user
@@ -353,7 +359,9 @@ This model function is **required** if the `authorization_code` grant is used.
| code.authorizationCode | string | The authorization code to be saved. |
| code.expiresAt | Date | The expiry time of the authorization code. |
| code.redirectUri | string | The redirect URI associated with the authorization code. |
-| code.scope | Array.<string> | The authorized scope of the authorization code. |
+| [code.codeChallenge] | string | PKCE challenge; dropping it prevents PKCE exchange. |
+| [code.codeChallengeMethod] | string | PKCE method; preserve when loading. |
+| [code.scope] | Array.<string> | The authorized scope of the authorization code. |
| client | [ClientData](#ClientData) | The client associated with the authorization code. |
| user | object | The user associated with the authorization code. |
@@ -365,6 +373,8 @@ function saveAuthorizationCode(code, client, user) {
authorization_code: code.authorizationCode,
expires_at: code.expiresAt,
redirect_uri: code.redirectUri,
+ code_challenge: code.codeChallenge,
+ code_challenge_method: code.codeChallengeMethod,
scope: code.scope,
client_id: client.id,
user_id: user.id
@@ -375,6 +385,8 @@ function saveAuthorizationCode(code, client, user) {
authorizationCode: authorizationCode.authorization_code,
expiresAt: authorizationCode.expires_at,
redirectUri: authorizationCode.redirect_uri,
+ codeChallenge: authorizationCode.code_challenge,
+ codeChallengeMethod: authorizationCode.code_challenge_method,
scope: authorizationCode.scope,
client: {id: authorizationCode.client_id},
user: {id: authorizationCode.user_id}
@@ -388,10 +400,10 @@ function saveAuthorizationCode(code, client, user) {
Invoked to revoke a refresh token.
This model function is **required** if the `refresh_token` grant is used.
**Invoked during:**
-- `refresh_token` grant
+- `refresh_token` grant (hook required even when rotation is disabled; invocation is skipped then)
**Remarks:**
-`token` is the refresh token object previously obtained through `Model#getRefreshToken() `.
+`token` is the refresh token object previously obtained through [Model#getRefreshToken](model.md#Model+getRefreshToken).
**Kind**: instance method of [Model](#Model)
**Returns**: Promise.<boolean> - Return `true` if the revocation was successful or `false` if the refresh token could not be found.
@@ -434,15 +446,16 @@ This model function is **required** if the `authorization_code` grant is used.
### model.verifyScope(accessToken, scope) โ Promise.<boolean>
Invoked during request authentication to check if the provided access token was authorized the requested scopes.
-This model function is **required** if scopes are used with `OAuth2Server#authenticate() `
-but it's never called, if you provide your own `authenticateHandler` to the options.
+This model function is **required** if scopes are used with [OAuth2Server#authenticate](server.md#OAuth2Server+authenticate)
+A custom `authenticateHandler` bypasses this hook only for `authorize`.
+`OAuth2Server.authenticate` always uses built-in authentication.
**Invoked during:**
- request authentication
**Remarks:**
-- `token` is the access token object previously obtained through `Model#getAccessToken() `.
-- `scope` is the required scope as given to `OAuth2Server#authenticate() ` as `options.scope`.
+- `token` is the access token object previously obtained through [Model#getAccessToken](model.md#Model+getAccessToken).
+- `scope` is the required scope as given to [OAuth2Server#authenticate](server.md#OAuth2Server+authenticate) as `options.scope`.
**Kind**: instance method of [Model](#Model)
**Returns**: Promise.<boolean> - Returns `true` if the access token passes, `false` otherwise.
@@ -468,7 +481,7 @@ function verifyScope(token, requestedScopes) {
Invoked to generate a new access token.
This model function is **optional**.
-If not implemented, a default handler is used that generates access tokens consisting of 40 characters in the range of `a..z0..9`.
+If not implemented, a default handler is used that generates access tokens consisting of 64 lowercase hexadecimal characters (`0..9a..f`), encoding 32 random bytes.
[RFC 6749 Appendix A.12](https://www.rfc-editor.org/rfc/rfc6749#appendix-A.12>) specifies that access tokens must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters).
**Invoked during:**
@@ -478,8 +491,8 @@ If not implemented, a default handler is used that generates access tokens consi
- `password` grant
**Remarks:**
-- `client` is the object previously obtained through `Model#getClient() `.
-- `user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getUserFromClient() ` (client credentials grant), `Model#getRefreshToken() ` (`token.user`; refresh token grant) or `Model#getUser() ` (password grant).
+- `client` is the object previously obtained through [Model#getClient](model.md#Model+getClient).
+- `user` is the user object previously obtained through [Model#getAuthorizationCode](model.md#Model+getAuthorizationCode) (`code.user`; authorization code grant), [Model#getUserFromClient](model.md#Model+getUserFromClient) (client credentials grant), [Model#getRefreshToken](model.md#Model+getRefreshToken) (`token.user`; refresh token grant) or [Model#getUser](model.md#Model+getUser) (password grant).
**Kind**: instance method of [Model](#Model)
**Returns**: Promise.<string> - A `String` to be used as access token.
@@ -495,7 +508,7 @@ If not implemented, a default handler is used that generates access tokens consi
### model.generateRefreshToken(client, user, scope) โ Promise.<string>
Invoked to generate a new refresh token.
-This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 40 characters in the range of `a..z0..9`.
+This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 64 lowercase hexadecimal characters (`0..9a..f`), encoding 32 random bytes.
[RFC 6749 Appendix A.17](https://www.rfc-editor.org/6749#appendix-A.17) specifies that refresh tokens must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters).
**Invoked during:**
@@ -506,9 +519,9 @@ This model function is **optional**. If not implemented, a default handler is us
**Remarks:**
-`client` is the object previously obtained through `Model#getClient() `.
+`client` is the object previously obtained through [Model#getClient](model.md#Model+getClient).
-`user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getRefreshToken() ` (`token.user`; refresh token grant) or `Model#getUser() ` (password grant).
+`user` is the user object previously obtained through [Model#getAuthorizationCode](model.md#Model+getAuthorizationCode) (`code.user`; authorization code grant), [Model#getRefreshToken](model.md#Model+getRefreshToken) (`token.user`; refresh token grant) or [Model#getUser](model.md#Model+getUser) (password grant).
**Kind**: instance method of [Model](#Model)
**Returns**: Promise.<string> - A `String` to be used as refresh token.
@@ -523,7 +536,7 @@ This model function is **optional**. If not implemented, a default handler is us
### model.generateAuthorizationCode(client, user, scope) โ Promise.<string>
Invoked to generate a new authorization code.
-This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 40 characters in the range of `a..z0..9`.
+This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 64 lowercase hexadecimal characters (`0..9a..f`), encoding 32 random bytes.
[RFC 6749 Appendix A.11](https://www.rfc-editor.org/6749#appendix-A.11) specifies that authorization codes must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters).
**Invoked during:**
@@ -541,7 +554,7 @@ This model function is **optional**. If not implemented, a default handler is us
-### model.validateScope(user, client, scope) โ Promise.<boolean>
+### model.validateScope(user, client, [scope]) โ Promise.<(Array.<string>\|null\|undefined\|false\|0)>
Invoked to check if the requested `scope` is valid for a particular `client`/`user` combination.
This model function is **optional**. If not implemented, any scope is accepted.
@@ -554,26 +567,28 @@ This model function is **optional**. If not implemented, any scope is accepted.
**Remarks:**
-`user` is the user object previously obtained through `Model#getAuthorizationCode() ` (`code.user`; authorization code grant), `Model#getUserFromClient() ` (client credentials grant) or `Model#getUser() ` (password grant).
+`user` is the user object previously obtained through [Model#getAuthorizationCode](model.md#Model+getAuthorizationCode) (`code.user`; authorization code grant), [Model#getUserFromClient](model.md#Model+getUserFromClient) (client credentials grant) or [Model#getUser](model.md#Model+getUser) (password grant).
-`client` is the object previously obtained through `Model#getClient ` (all grants).
+`client` is the object previously obtained through [Model#getClient](model.md#Model+getClient) (all grants).
You can decide yourself whether you want to reject or accept partially valid scopes by simply filtering out invalid scopes and returning only the valid ones.
+At the authorization endpoint, `user` comes from the custom authenticate handler
+or the built-in bearer fallback; during code exchange it comes from the stored code.
**Kind**: instance method of [Model](#Model)
-**Returns**: Promise.<boolean> - Validated scopes to be used or a falsy value to reject the requested scopes.
+**Returns**: Promise.<(Array.<string>\|null\|undefined\|false\|0)> - Accepted scope array, or a falsy rejection. Synchronous values are also supported. Returning `true` is not a usable scope; `verifyScope` has the separate boolean contract.
| Param | Type | Description |
| --- | --- | --- |
| user | object | The associated user. |
| client | [ClientData](#ClientData) | The associated client. |
-| scope | Array.<string> | The scopes to validate. |
+| [scope] | Array.<string> | The scopes to validate; omitted scope is undefined. |
**Example**
```js
// To reject invalid or only partially valid scopes:
const VALID_SCOPES = ['read', 'write'];
-function validateScope(user, client, scope) {
+function validateScope(user, client, scope = []) {
if (!scope.every(s => VALID_SCOPES.indexOf(s) >= 0)) {
return false;
}
@@ -584,7 +599,7 @@ function validateScope(user, client, scope) {
```js
// To accept partially valid scopes:
const VALID_SCOPES = ['read', 'write'];
-function validateScope(user, client, scope) {
+function validateScope(user, client, scope = []) {
return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0);
}
```
@@ -634,7 +649,7 @@ An `Object` representing the access token and associated data. `token.client` an
| --- | --- | --- |
| accessToken | string | The access token passed to `getAccessToken()` |
| accessTokenExpiresAt | Date | The expiry time of the access token. |
-| scope | Array.<string> | The authorized scope of the access token. |
+| [scope] | Array.<string> | The authorized scope of the access token. |
| client | object | The client associated with the access token. |
| client.id | string | A unique string identifying the client. |
| user | object | The user associated with the access token. |
@@ -650,8 +665,8 @@ An `Object` representing the refresh token and associated data. `token.client` a
| Name | Type | Description |
| --- | --- | --- |
| refreshToken | string | The refresh token passed to `getRefreshToken()` |
-| refreshTokenExpiresAt | Date | The expiry time of the refresh token. |
-| scope | Array.<string> | The authorized scope of the refresh token. |
+| [refreshTokenExpiresAt] | Date | The expiry time of the refresh token. |
+| [scope] | Array.<string> | The authorized scope of the refresh token. |
| client | [ClientData](#ClientData) | The client associated with the refresh token. |
| user | object | The user associated with the access token. |
@@ -665,10 +680,12 @@ An `Object` representing the authorization code and associated data. `code.clien
| Name | Type | Description |
| --- | --- | --- |
+| [codeChallenge] | string | Stored PKCE challenge; preserve during save/load. |
+| [codeChallengeMethod] | string | Stored PKCE method (S256 or plain). |
| authorizationCode | string | The authorization code passed to `getAuthorizationCode()`. |
| expiresAt | Date | The expiry time of the authorization code. |
| redirectUri | string | The redirect URI of the authorization code. |
-| scope | Array.<string> | The authorized scope of the authorization code. |
+| [scope] | Array.<string> | The authorized scope of the authorization code. |
| client | [ClientData](#ClientData) | The client associated with the authorization code. |
| user | object | The user associated with the access token. |
@@ -685,6 +702,6 @@ An `Object` representing the client and associated data.
| id | string | A unique string identifying the client. |
| redirectUris | Array.<string> | Redirect URIs allowed for the client. Required for the `authorization_code` grant. |
| grants | Array.<string> | Grant types allowed for the client. |
-| accessTokenLifetime | number | Client-specific lifetime of generated access tokens in seconds. |
-| refreshTokenLifetime | number | Client-specific lifetime of generated refresh tokens in seconds. |
+| [accessTokenLifetime] | number | Client-specific lifetime of generated access tokens in seconds. |
+| [refreshTokenLifetime] | number | Client-specific lifetime of generated refresh tokens in seconds. |
diff --git a/docs/api/pkce/pkce.md b/docs/api/pkce/pkce.md
index 9d8e7adc..7b549349 100644
--- a/docs/api/pkce/pkce.md
+++ b/docs/api/pkce/pkce.md
@@ -22,22 +22,23 @@ DIGIT = %x30-39
## pkce
* [pkce](#module_pkce)
- * [~getHashForCodeChallenge(method, verifier)](#module_pkce..getHashForCodeChallenge) โ String \| undefined
+ * [~getHashForCodeChallenge(options)](#module_pkce..getHashForCodeChallenge) โ String \| undefined
* [~codeChallengeMatchesABNF(codeChallenge)](#module_pkce..codeChallengeMatchesABNF) โ Boolean
- * [~isPKCERequest(grantType, codeVerifier)](#module_pkce..isPKCERequest) โ boolean
+ * [~isPKCERequest(options)](#module_pkce..isPKCERequest) โ boolean
* [~isValidMethod(method)](#module_pkce..isValidMethod) โ boolean
-### pkce~getHashForCodeChallenge(method, verifier) โ String \| undefined
+### pkce~getHashForCodeChallenge(options) โ String \| undefined
Return hash for code-challenge method-type.
**Kind**: inner method of [pkce](#module_pkce)
| Param | Type | Description |
| --- | --- | --- |
-| method | String | the code challenge method |
-| verifier | String | the code_verifier |
+| options | object | Inputs, e.g. `getHashForCodeChallenge({method: 'S256', verifier})`. |
+| options.method | String | the code challenge method |
+| options.verifier | String | the code_verifier |
@@ -58,22 +59,23 @@ DIGIT = %x30-39
-### pkce~isPKCERequest(grantType, codeVerifier) โ boolean
-Check if the request is a PCKE request. We assume PKCE if grant type is
+### pkce~isPKCERequest(options) โ boolean
+Check if the request is a PKCE request. We assume PKCE if grant type is
'authorization_code' and code verifier is present.
**Kind**: inner method of [pkce](#module_pkce)
-| Param | Type |
-| --- | --- |
-| grantType | String |
-| codeVerifier | String |
+| Param | Type | Description |
+| --- | --- | --- |
+| options | object | Inputs, e.g. `isPKCERequest({grantType: 'authorization_code', codeVerifier})`. |
+| options.grantType | String | |
+| options.codeVerifier | String | |
### pkce~isValidMethod(method) โ boolean
Checks if the code challenge method is one of the supported methods
-'sha256' or 'plain'
+'S256' or 'plain'
**Kind**: inner method of [pkce](#module_pkce)
diff --git a/docs/api/request.md b/docs/api/request.md
index 8360100f..752e9a70 100644
--- a/docs/api/request.md
+++ b/docs/api/request.md
@@ -8,14 +8,15 @@ request signature.
**Kind**: global class
* [Request](#Request)
- * [new Request(headers, method, query, [body], ...otherOptions)](#new_Request_new)
- * [.get(field)](#Request+get) โ string
- * [.is(...types)](#Request+is) โ boolean
+ * [new Request(options)](#new_Request_new)
+ * [.get(field)](#Request+get) โ string \| undefined
+ * [.is(...types)](#Request+is) โ string \| false
-### new Request(headers, method, query, [body], ...otherOptions)
+### new Request(options)
Creates a new request instance
+Additional properties on options are copied onto the wrapper.
**Throws**:
@@ -24,22 +25,20 @@ Creates a new request instance
| Param | Type | Description |
| --- | --- | --- |
-| headers | object | key-value object of headers |
-| method | string | the HTTP method |
-| query | object | key-value object of query parameters |
-| [body] | object | optional key-value object of body parameters |
-| ...otherOptions | object | any other properties that should be assigned to the request by your webserver |
+| options | object | Wrapper options. |
+| options.headers | object | key-value object of headers |
+| options.method | string | the HTTP method |
+| options.query | object | key-value object of query parameters |
+| [options.body] | object | optional key-value object of body parameters |
**Example**
```js
-function (req, res, next) {
- // most webservers follow a similar structure
- const response = new Request(req);
-}
+const { Request } = require('@node-oauth/oauth2-server');
+const request = new Request({ method: 'GET', headers: {}, query: {} });
```
-### request.get(field) โ string
+### request.get(field) โ string \| undefined
Get a request header (case-insensitive).
**Kind**: instance method of [Request](#Request)
@@ -50,7 +49,7 @@ Get a request header (case-insensitive).
-### request.is(...types) โ boolean
+### request.is(...types) โ string \| false
Check if the content-type matches any of the given mime types.
**Kind**: instance method of [Request](#Request)
diff --git a/docs/api/response-types/token-response-type.md b/docs/api/response-types/token-response-type.md
index ee57ef7f..9dd12b66 100644
--- a/docs/api/response-types/token-response-type.md
+++ b/docs/api/response-types/token-response-type.md
@@ -1,4 +1,7 @@
## TokenResponseType
+Unsupported internal placeholder, not a package export. Construction throws
+`ServerError('Not implemented.')`. Only bearer tokens and code authorization responses are supported.
+
**Kind**: global class
diff --git a/docs/api/response.md b/docs/api/response.md
index e0b04020..b33f60c5 100644
--- a/docs/api/response.md
+++ b/docs/api/response.md
@@ -8,30 +8,28 @@ response signature.
**Kind**: global class
* [Response](#Response)
- * [new Response(headers, method, [body], ...otherOptions)](#new_Response_new)
+ * [new Response([options])](#new_Response_new)
* [.get(field)](#Response+get) โ string \| undefined
* [.redirect(url)](#Response+redirect)
* [.set(field, value)](#Response+set)
-### new Response(headers, method, [body], ...otherOptions)
+### new Response([options])
Create a new Response instance.
+Additional properties on options are copied onto the wrapper.
| Param | Type | Description |
| --- | --- | --- |
-| headers | object | key-value object of headers |
-| method | string | the HTTP method |
-| [body] | object | optional key-value object of body parameters |
-| ...otherOptions | object | any other properties that should be assigned to the request by your webserver |
+| [options] | object | Wrapper options. |
+| [options.headers] | object | key-value object of headers |
+| [options.body] | object | optional key-value object of body parameters |
**Example**
```js
-function (req, res, next) {
- // most webservers follow a similar structure
- const response = new Response(res);
-}
+const { Response } = require('@node-oauth/oauth2-server');
+const response = new Response({ headers: {}, body: {} });
```
diff --git a/docs/api/server.md b/docs/api/server.md
index 29f0333e..9675464b 100644
--- a/docs/api/server.md
+++ b/docs/api/server.md
@@ -7,7 +7,7 @@ The main OAuth2 server class.
* [OAuth2Server](#OAuth2Server)
* [new OAuth2Server(options)](#new_OAuth2Server_new)
- * [.authenticate()](#OAuth2Server+authenticate) โ Promise.<object>
+ * [.authenticate(request, response, [options])](#OAuth2Server+authenticate) โ Promise.<object>
* [.authorize(request, response, [options])](#OAuth2Server+authorize) โ Promise.<object>
* [.token(request, response, [options])](#OAuth2Server+token) โ Promise.<object>
@@ -29,7 +29,7 @@ Instantiates `OAuth2Server` using the supplied model.
| --- | --- | --- | --- |
| options | object | | Server options. |
| options.model | Model | | The Model; this is always required. |
-| options.scope | Array.<string> \| undefined | | The scope(s) to authenticate. |
+| options.scope | string \| Array.<string> \| undefined | | The scope(s) to authenticate. |
| [options.addAcceptedScopesHeader] | boolean | true | Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. |
| [options.addAuthorizedScopesHeader] | boolean | true | Set the `X-OAuth-Scopes` HTTP header on response objects. |
| [options.allowBearerTokensInQueryString] | boolean | false | Allow clients to pass bearer tokens in the query string of a request. |
@@ -40,7 +40,7 @@ Instantiates `OAuth2Server` using the supplied model.
| [options.accessTokenLifetime] | number | 3600 | Lifetime of generated access tokens in seconds (default = 1 hour). |
| [options.refreshTokenLifetime] | number | 1209600 | Lifetime of generated refresh tokens in seconds (default = 2 weeks). |
| [options.allowExtendedTokenAttributes] | boolean | false | Allow extended attributes to be set on the returned token (see remarks section). |
-| [options.requireClientAuthentication] | object \| boolean | object | Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. |
+| [options.requireClientAuthentication] | Object.<string, boolean> | object | Per-grant presence-check map, e.g. `{refresh_token: false}`. Unlisted grants default to true; a global `false` does not disable checks. |
| [options.alwaysIssueNewRefreshToken] | boolean | true | Always revoke the used refresh token and issue a new one for the `refresh_token` grant. |
| [options.extendedGrantTypes] | object | object | Additional supported grant types. |
| [options.enablePlainPKCE] | boolean | false | Allow the use of the `plain` code challenge method for PKCE. This is not recommended for production environments. |
@@ -52,8 +52,9 @@ const OAuth2Server = require('@node-oauth/oauth2-server');
```
-### oAuth2Server.authenticate() โ Promise.<object>
-Authenticates a request.
+### oAuth2Server.authenticate(request, response, [options]) โ Promise.<object>
+Authenticates a request. Sets selected headers, but not an error status/body.
+The integration must handle rejection and send its own error response.
**Kind**: instance method of [OAuth2Server](#OAuth2Server)
**Returns**: Promise.<object> - A `Promise` that resolves to the access token object returned from the model's `getAccessToken`.
@@ -65,14 +66,18 @@ Authenticates a request.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
-| options.scope | Array.<string> \| undefined | | The scope(s) to authenticate. |
+| request | Request | | The wrapped request. |
+| response | Response | | The wrapped response. |
+| [options] | object | | Authentication options. |
+| options.scope | string \| Array.<string> \| undefined | | The scope(s) to authenticate. |
| [options.addAcceptedScopesHeader] | boolean | true | Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. |
| [options.addAuthorizedScopesHeader] | boolean | true | Set the `X-OAuth-Scopes` HTTP header on response objects. |
| [options.allowBearerTokensInQueryString] | boolean | false | Allow clients to pass bearer tokens in the query string of a request. |
**Example**
```js
-const oauth = new OAuth2Server({model: ...});
+const { Request, Response } = OAuth2Server;
+const oauth = new OAuth2Server({model: require('./model')});
function authenticateHandler(options) {
return function(req, res, next) {
let request = new Request(req);
@@ -91,22 +96,19 @@ function authenticateHandler(options) {
### oAuth2Server.authorize(request, response, [options]) โ Promise.<object>
-Authorizes a token request.
+Authorizes an authorization-code request.
**Remarks:**
-If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`.
+If `request.query.allowed` or `request.body.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`.
-In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied.
+Without `options.authenticateHandler`, authorization uses built-in bearer authentication.
+For session-based login, supply a custom handler.
The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object.
-If there is no associated user (i.e. the user is not logged in) a falsy value should be returned.
+A falsy result causes `ServerError` (default status 503), not a login challenge.
+Complete login first or explicitly handle that error locally.
+Prepared redirects must be forwarded on success and rejection; earlier client/user
+failures may have no redirect. See [response handling](../guide/getting-started.md#sending-the-response).
-```js
-let authenticateHandler = {
- handle: function(request, response) {
- return // get authenticated user;
- }
-};
-```
When working with a session-based login mechanism, the handler can simply look like this:
```js
let authenticateHandler = {
@@ -121,7 +123,7 @@ let authenticateHandler = {
In case of an error, the promise rejects with one of the error types derived from `OAuthError`.
**Throws**:
-- AccessDeniedError The resource owner denied the access request (i.e. `request.query.allow` was `'false'`).
+- AccessDeniedError The resource owner denied the access request (i.e. `request.query.allowed` or `request.body.allowed` was `'false'`).
| Param | Type | Default | Description |
@@ -137,7 +139,8 @@ let authenticateHandler = {
**Example**
```js
-const oauth = new OAuth2Server({model: ...});
+const { Request, Response } = OAuth2Server;
+const oauth = new OAuth2Server({model: require('./model')});
function authorizeHandler(options) {
return function(req, res, next) {
let request = new Request(req);
@@ -157,9 +160,12 @@ function authorizeHandler(options) {
### oAuth2Server.token(request, response, [options]) โ Promise.<object>
Retrieves a new token for an authorized token request.
+Success populates token JSON and cache headers. Caught failures populate error
+status/body; early method/content-type failures do not. The integration must
+handle rejection and send the response, including headers.
**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`. 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`.
+If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from [Model#saveToken](model.md#Model+saveToken) are copied to the token response sent to the client.
+By default, all grant types require the client to send its `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 the presence check for `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](model.md#Model+getClient) implementation for token requests. Authorization also calls this hook with a null secret, so use endpoint-specific model wrappers; see the [PKCE guide](../guide/pkce.md#pkce-client-authentication-and-refresh-tokens). A global `requireClientAuthentication: false` is not a supported switch.
```js
let options = {
// ...
@@ -194,19 +200,20 @@ For information on how to implement a handler for a custom grant type see the ex
| [options.accessTokenLifetime] | number | 3600 | Lifetime of generated access tokens in seconds (default = 1 hour). |
| [options.refreshTokenLifetime] | number | 1209600 | Lifetime of generated refresh tokens in seconds (default = 2 weeks). |
| [options.allowExtendedTokenAttributes] | boolean | false | Allow extended attributes to be set on the returned token (see remarks section). |
-| [options.requireClientAuthentication] | object \| boolean | object | Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. |
+| [options.requireClientAuthentication] | Object.<string, boolean> | object | Per-grant presence-check map, e.g. `{refresh_token: false}`. Unlisted grants default to true; a global `false` does not disable checks. |
| [options.alwaysIssueNewRefreshToken] | boolean | true | Always revoke the used refresh token and issue a new one for the `refresh_token` grant. |
| [options.extendedGrantTypes] | object | object | Additional supported grant types. |
**Example**
```js
-const oauth = new OAuth2Server({model: ...});
+const { Request, Response } = OAuth2Server;
+const oauth = new OAuth2Server({model: require('./model')});
function tokenHandler(options) {
return function(req, res, next) {
let request = new Request(req);
let response = new Response(res);
return oauth.token(request, response, options)
- .then(function(code) {
+ .then(function(token) {
res.locals.oauth = {token: token};
next();
})
diff --git a/docs/api/token-types/mac-token-type.md b/docs/api/token-types/mac-token-type.md
index dd87d949..fb41b29e 100644
--- a/docs/api/token-types/mac-token-type.md
+++ b/docs/api/token-types/mac-token-type.md
@@ -1,4 +1,7 @@
## MacTokenType
+Unsupported internal placeholder, not a package export. Construction throws
+`ServerError('Not implemented.')`. Only bearer tokens and code authorization responses are supported.
+
**Kind**: global class
diff --git a/docs/api/utils/crypto-util.md b/docs/api/utils/crypto-util.md
index 19d943d3..1f506075 100644
--- a/docs/api/utils/crypto-util.md
+++ b/docs/api/utils/crypto-util.md
@@ -3,17 +3,18 @@
## CryptoUtil
-### CryptoUtil~createHash(algorithm, data, [encoding], output) โ Buffer \| string
+### CryptoUtil~createHash(options) โ Buffer \| string
Creates a new hash by given algorithm, data and digest encoding.
Defaults to sha256.
**Kind**: inner method of [CryptoUtil](#module_CryptoUtil)
**Returns**: Buffer \| string - if {output} is undefined, a {Buffer} is returned, otherwise a {String}
-| Param | Type | Description |
-| --- | --- | --- |
-| algorithm | string | the hash algorithm, default is 'sha256' |
-| data | Buffer \| string \| TypedArray \| DataView | the data to hash |
-| [encoding] | string | optional, the encoding of the input |
-| output | 'base64' \| 'base64url' \| 'binary' \| 'hex' \| undefined | optional, the desired output type |
+| Param | Type | Default | Description |
+| --- | --- | --- | --- |
+| options | object | | Hash inputs, e.g. `createHash({data: 'hello', output: 'hex'})`. |
+| [options.algorithm] | string | "'sha256'" | the hash algorithm |
+| options.data | Buffer \| string \| TypedArray \| DataView | | the data to hash |
+| [options.encoding] | string | | optional, the encoding of the input |
+| [options.output] | 'base64' \| 'base64url' \| 'binary' \| 'hex' | | optional, the desired output type |
diff --git a/docs/api/utils/token-util.md b/docs/api/utils/token-util.md
index b1c8ca04..aa27b0f3 100644
--- a/docs/api/utils/token-util.md
+++ b/docs/api/utils/token-util.md
@@ -4,6 +4,6 @@
### TokenUtil~generateRandomToken() โ Promise.<string>
-Generates random token as 32 byte hex string.
+Generates 32 random bytes encoded as 64 lowercase hexadecimal characters (0..9a..f).
**Kind**: inner method of [TokenUtil](#module_TokenUtil)
diff --git a/docs/build-api.js b/docs/build-api.js
index d0b64178..c056a398 100644
--- a/docs/build-api.js
+++ b/docs/build-api.js
@@ -5,11 +5,11 @@ const glob = require('glob');
(async function () {
// Usage:
- // node scripts/build-docs-cli.js [srcRoot] [outRoot]
+ // node docs/build-api.js [srcRoot] [outRoot]
// Examples:
- // node scripts/build-docs-cli.js -> src: "src", out: "docs"
- // node scripts/build-docs-cli.js lib -> src: "lib", out: "docs"
- // node scripts/build-docs-cli.js lib build/docs -> src: "lib", out: "build/docs"
+ // node docs/build-api.js -> src: "lib/", out: "docs/api/"
+ // node docs/build-api.js lib -> src: "lib", out: "docs/api/"
+ // node docs/build-api.js lib build/docs -> src: "lib", out: "build/docs"
const srcRoot = process.argv[2] || 'lib/';
const outRoot = process.argv[3] || 'docs/api/';
@@ -39,4 +39,4 @@ const glob = require('glob');
console.error('Failed:', src, err.message);
}
}
-})();
\ No newline at end of file
+})();
diff --git a/docs/guide/adapters.md b/docs/guide/adapters.md
index 404f9bbf..675ebcb0 100644
--- a/docs/guide/adapters.md
+++ b/docs/guide/adapters.md
@@ -9,14 +9,17 @@ The *@node-oauth/oauth2-server* module is typically not used directly but throug
Adapters typically do the following:
-- Inherit from `OAuth2Server `.
+- Inherit from [OAuth2Server](../api/server.md).
-- Override `authenticate() `, `authorize() ` and `token() `.
+- Override [authenticate](../api/server.md#OAuth2Server+authenticate), [authorize](../api/server.md#OAuth2Server+authorize) and [token](../api/server.md#OAuth2Server+token).
Each of these functions should:
- - Create `Request ` and `Response ` objects from their framework-specific counterparts.
+ - Create [Request](../api/request.md) and [Response](../api/response.md) objects from their framework-specific counterparts.
- Call the original function.
- - Copy all fields from the `Response ` back to the framework-specific request object and send it.
+ - Copy all fields from the [Response](../api/response.md) back to the framework-specific response object and send it.
Adapters should preserve functionality provided by *@node-oauth/oauth2-server* but are free to add additional features that make sense for the respective HTTP server framework.
+
+Handle rejected promises and preserve prepared authorization redirects as described
+in [Sending the response](./getting-started.md#sending-the-response).
diff --git a/docs/guide/compliance.md b/docs/guide/compliance.md
index 67e33081..3bc568dd 100644
--- a/docs/guide/compliance.md
+++ b/docs/guide/compliance.md
@@ -2,7 +2,7 @@
`@node-oauth/oauth2-server` implements the OAuth 2.0 *protocol logic* โ the grant
flows, authorization-code and token validation, scope checks, PKCE verification,
-and RFC-compliant success/error responses. It is deliberately **framework-agnostic
+and response preparation. It is deliberately **framework-agnostic
and transport-agnostic**: it never touches the network, your HTTP framework, or
your storage directly.
@@ -20,17 +20,22 @@ So you know where the boundary sits, the library takes care of:
- The `authorization_code`, `client_credentials`, `refresh_token` and `password`
grant flows, plus extension grants.
-- Validating authorization codes, refresh tokens and access tokens (existence,
- ownership and expiry), and enforcing single use of authorization codes.
-- Scope parsing/validation and PKCE verification (see [PKCE](./pkce.md)).
-- Reading request parameters from **both** the query string and the request body.
-- Requiring the token endpoint to use `POST` with
- `Content-Type: application/x-www-form-urlencoded`.
-- RFC-compliant responses: the `error` / `error_description` body on failures
- ([ยง5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2)),
- `Cache-Control: no-store` and `Pragma: no-cache` on token responses
- ([ยง5.1](https://www.rfc-editor.org/rfc/rfc6749#section-5.1)), and the
- `WWW-Authenticate` header on `401` responses.
+- Authorization-code existence, client ownership, required expiry Date and expiry,
+ followed by model revocation. Single use depends on durable, atomic revocation.
+- Refresh-token existence, client ownership and user presence; refresh expiry is
+ checked only when present and must then be a Date.
+- Access-token existence, user presence and required expiry Date. Authentication
+ does not independently bind the token to a requesting client.
+- Scope parsing, configured scope checks and PKCE verification (see [PKCE](./pkce.md)).
+- Authorization parameters generally from body or query. Token grant type,
+ code, verifier, refresh token and password credentials come from the body;
+ client credentials come from HTTP Basic or the body. Code exchange also
+ accepts `redirect_uri` in the query.
+- Token requests must use `POST` and `application/x-www-form-urlencoded`.
+- Successful token responses have `Cache-Control: no-store` and `Pragma: no-cache`.
+ Caught token failures get error bodies, but early failures do not. Authentication
+ sets challenge headers for selected errors, not a complete error response.
+ See [response responsibilities](./getting-started.md#sending-the-response).
## What you must handle
@@ -59,7 +64,8 @@ endpoint, by contrast, MUST be `POST`, which the library enforces.
### Send the response and perform redirects
-The library *populates* the `Response` object; it does not send it. You must copy
+Response preparation varies by endpoint and failure stage; see
+[Sending the response](./getting-started.md#sending-the-response). The library does not send it. You must copy
the status, headers and body onto your framework's real response object and, for
the authorization endpoint, issue the redirect (the `location` header is set for
you). Do not reassign your framework's `req`/`res` to the library's
@@ -88,16 +94,20 @@ redirect matches the value it sent, remain your responsibility
### Authenticate the resource owner
-For the authorization endpoint you must supply an `authenticateHandler` that
-returns the logged-in user (see [Getting started](./getting-started.md)). The
-library does not implement user login or session management.
+Authorization uses built-in bearer authentication when no custom
+`authenticateHandler` is supplied. For session-based login, provide a handler
+returning the logged-in user and handle login first. A falsy handler result is a
+server error, not a login challenge. See [model requirements](./model.md#authorization-code-grant).
+The library does not implement login or session management.
### Secure storage in your model
Your [model](./model.md) is responsible for handling secrets and tokens safely:
hash and compare `client_secret`s, store authorization codes and tokens securely
-and honour their expiry, and validate `redirect_uri`s against the registered set
-(`validateRedirectUri`) โ see
+and provide registered `client.redirectUris`. The library defaults to exact
+inclusion checks; `validateRedirectUri` is an optional override. Implement atomic
+code/token revocation and enforce client authentication in the appropriate endpoint
+context (see [PKCE client authentication](./pkce.md#pkce-client-authentication-and-refresh-tokens)) โ see
[RFC 6749 ยง3.1.2](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2) and
[ยง10](https://www.rfc-editor.org/rfc/rfc6749#section-10).
@@ -108,7 +118,7 @@ and honour their expiry, and validate `redirect_uri`s against the registered set
- [ ] Framework response sent and redirects issued (see [Sending the response](./getting-started.md#sending-the-response)).
- [ ] **Rate limiting / brute-force** protection on the token and authorization endpoints.
- [ ] **CSRF** protection on the login/consent UI; `state` validated by clients.
-- [ ] Resource owner authenticated via an `authenticateHandler`.
+- [ ] Resource owner authenticated via bearer fallback or a custom `authenticateHandler`.
- [ ] Model stores secrets/codes/tokens securely and validates `redirect_uri`.
- [ ] **PKCE** used for the authorization code grant (see [PKCE](./pkce.md)).
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 2a91c014..0ade9d03 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -24,8 +24,8 @@ resources, before starting to operate your own authorization server:
- Supports `authorization code`, `client credentials`,
`refresh token` and `password` (deprecated) grant, as well as
`extension grants`, with scopes.
-- Can be used with *promises*, *ES6 generators* and *async*/*await*.
-- Fully `6749` and `6750` compliant.
+- Model hooks support synchronous values, native Promises and async/await.
+- Implements OAuth 2.0 flows with [integration responsibilities](./compliance.md).
- Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc.
- Complete [test suite](https://github.com/node-oauth/node-oauth2-server/tree/master/test).
@@ -83,34 +83,79 @@ oauth.authenticate(request, response)
### Sending the response
-This library is **framework-agnostic and does not send the HTTP response for you**.
-`authenticate()`, `authorize()` and `token()` resolve with their result *and*
-populate the `Response` object you passed in โ setting `response.status`,
-`response.headers` and `response.body`. It is up to you to copy those onto your
-framework's real response and send it. For `authorize()` this is how the redirect
-happens: the `location` header is set on the `Response`, but you still have to
-issue it.
+The library does not send HTTP responses. Copy the wrapper's headers to your
+framework response and handle each endpoint's contract:
-A minimal Express example for the authorization endpoint:
+- `authorize` prepares a 302 redirect on success and on errors inside its
+ redirect-capable block, then rejects on error. Forward a prepared redirect even
+ after rejection. Earlier client/user failures may leave no redirect.
+- `token` populates the token JSON on success and error JSON for failures inside
+ its catch block. Method/content-type checks occur before that block and can
+ leave the default 200/empty body. Success sets cache-control headers; error
+ handling does not set them.
+- `authenticate` returns the token and sets selected scope or challenge headers.
+ It does not set an error status/body. On success, continue to your protected
+ resource; on rejection, your integration must select the error response.
+
+The following Express example assumes `app`, `oauth`, `Request`, and
+`Response` from the setup above, plus login middleware that populates
+`req.session.user` before this route. Complete login before calling authorization.
```js
+const authenticateHandler = {
+ handle(request) {
+ return request.session.user;
+ }
+};
+
+// Use fixed public messages, never arbitrary exception text.
+function sendLocalError(res, err, response) {
+ const publicErrors = {
+ invalid_client: [400, 'Invalid client'],
+ invalid_request: [400, 'Invalid request'],
+ invalid_grant: [400, 'Invalid grant'],
+ unauthorized_client: [400, 'Unauthorized client'],
+ unsupported_grant_type: [400, 'Unsupported grant type'],
+ invalid_scope: [400, 'Invalid scope'],
+ invalid_token: [401, 'Invalid token'],
+ unauthorized_request: [401, 'Authentication required'],
+ insufficient_scope: [403, 'Insufficient scope'],
+ access_denied: [400, 'Access denied']
+ };
+ const known = err instanceof OAuth2Server.OAuthError && publicErrors[err.name];
+ const [status, description] = known || [500, 'Internal server error'];
+ res.set(response.headers);
+ return res.status(known && err.code === 401 ? 401 : status).json({
+ error: known ? err.name : 'server_error',
+ error_description: description
+ });
+}
+
app.get('/authorize', async (req, res) => {
- // Wrap the framework objects in NEW variables โ see the warning below.
const request = new Request(req);
- const response = new Response(res);
+ const response = new Response();
try {
await oauth.authorize(request, response, { authenticateHandler });
} catch (err) {
- return res.status(err.code || 500).json({ error: err.name, error_description: err.message });
+ if (response.status !== 302 || !response.get('location')) {
+ return sendLocalError(res, err, response);
+ }
}
- // The library populated `response`; now you send it. For authorize that is a redirect:
- res.set(response.headers); // includes the `location` header
- return res.status(response.status).end(); // 302 -> the browser follows `location`
+ // Includes denial redirects (allowed=false) and their state parameter.
+ res.set(response.headers);
+ return res.status(response.status).end();
});
```
+For a token route, await `oauth.token(request, response)` in a try/catch.
+On success send `res.set(response.headers).status(response.status).json(response.body)`.
+On rejection use a local handler such as `sendLocalError`; do not send the
+wrapper's default 200. Your integration can add token endpoint cache headers to
+both success and error responses. For authentication failures, also copy the
+wrapper's challenge headers before sending the selected local error response.
+
> **Do not reassign your framework's `req`/`res`** to the library's objects
> (e.g. `res = new Response(res)`). Doing so discards framework methods such as
> `res.redirect()`, and is a common cause of a request appearing to "hang".
diff --git a/docs/guide/grant-types.md b/docs/guide/grant-types.md
index b5827f49..5d2da637 100644
--- a/docs/guide/grant-types.md
+++ b/docs/guide/grant-types.md
@@ -45,7 +45,7 @@ username and password, typically using an interactive form).
**Defined in:** [Section 4.4 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.4).
-**Model requirements:** [Model for Refresh Token Code Grant](./model.md#refresh-token-grant)
+**Model requirements:** [Model for Client Credentials Grant](./model.md#client-credentials-grant)
The client can request an access token using only its client credentials (or other supported means of authentication)
when requesting access to the protected resources under its control.
@@ -53,7 +53,7 @@ The client credentials grant type **must** only be used by confidential clients.
## Extension Grants
-**Defined in:** [Section 4.5 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.4).
+**Defined in:** [Section 4.5 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.5).
Create a subclass of `AbstractGrantType` and create methods handle
and saveToken along with other required methods according to your needs:
@@ -82,14 +82,14 @@ class MyCustomGrantType extends AbstractGrantType {
}
async saveToken(user, client, scope) {
- this.validateScope(user, client, scope);
+ const validatedScope = await this.validateScope(user, client, scope);
let token = {
- accessToken: await this.generateAccessToken(client, user, scope),
+ accessToken: await this.generateAccessToken(client, user, validatedScope),
accessTokenExpiresAt: this.getAccessTokenExpiresAt(),
- refreshToken: await this.generateRefreshToken(client, user, scope),
+ refreshToken: await this.generateRefreshToken(client, user, validatedScope),
refreshTokenExpiresAt: this.getRefreshTokenExpiresAt(),
- scope: scope
+ scope: validatedScope
};
return this.model.saveToken(token, client, user);
@@ -104,7 +104,10 @@ module.exports = MyCustomGrantType;
```
Extension grants are registered
-through [OAuth2Server#token](../api/server.md#oauth2servertokenrequest-response-options--codepromiseobjectcode) (
+through [OAuth2Server#token](../api/server.md#OAuth2Server+token) (
`options.extendedGrantTypes`).
-This might require you to approve the new `grant_type` for a particular `client` if you do checks on valid grant types.
+The extension URI must also appear in `client.grants`; the token handler always
+rejects grants not approved for that client. Implement `getUserBySomething` using
+your application's credential validation. Awaiting scope validation prevents token
+storage on rejection and ensures any reduced scope is generated, saved and returned.
diff --git a/docs/guide/migrating-to-v5.md b/docs/guide/migrating-to-v5.md
index 151f806d..f881b01e 100644
--- a/docs/guide/migrating-to-v5.md
+++ b/docs/guide/migrating-to-v5.md
@@ -26,9 +26,12 @@ since callback support is dropped.
Note: Synchronous model functions are still supported. However, we recommend to use Promise or async,
if database operations (or other heavy operations) are part of a specific model function implementation.
-## Scope is now Array
+## Scope arrays inside the library
-In earlier versions we allowed scope to be strings with words, separated by empty space.
-With beginning of 5.0.0 the scope parameter needs to be an Array of strings.
+Model hooks use arrays such as `['read', 'write']`. HTTP requests still use a
+space-delimited string, for example `scope=read%20write`, and token JSON contains
+`"scope": "read write"`. HTTP scope arrays are unsupported and rejected.
-This implies to requests, responses and model implementations where scope is included.
+Omitted or null HTTP scope becomes `undefined` in hooks. Optional scope validators
+should account for this, for example by selecting a default scope array. The
+JavaScript `authenticate` option `scope` separately accepts a string or an array.
diff --git a/docs/guide/model.md b/docs/guide/model.md
index 2566be25..d4844fb9 100644
--- a/docs/guide/model.md
+++ b/docs/guide/model.md
@@ -1,96 +1,76 @@
# Model Overview
-[OAuth2Server](../api/server.md) requires a model object through which some aspects of storage,
-retrieval and custom validation are abstracted.
-
-## Migration Notes
-
-**Version \>=5.x:** Callback support has been removed! Each model function supports either sync or async
-(`Promise` or `async function`) return values.
-
-**Version \<=4.x:** Each model function supports *promises*, *Node-style callbacks*, *ES6 generators*
-and *async*/*await* (using [Babel](https://babeljs.io)). Note that promise support implies support for returning plain
-values where asynchronism is not required.
+[OAuth2Server](../api/server.md) accepts a plain object of storage and validation
+hooks. Hooks may return synchronous values, native Promises, or use async/await.
+Direct generator functions are unsupported; application generator code needs a
+runner that returns a supported value or Promise. Callback support ended in v5.
## Request Authentication
-See [Section 2 of RFC 6750](https://www.rfc-editor.org/rfc/rfc6750#section-2)
-
-The authorization server authenticates requests that are
-sent to the resource server by verifying the included bearer token.
-
-Model functions used during request authentication:
-
-- [getAccessToken](../api/model.md#modelgetaccesstokenaccesstoken--code-promiseaccesstokendata-code)
-- [verifyScope](../api/model.md#modelverifyscopeaccesstoken-scope--codepromisebooleancode)
-
+`authenticate` requires [getAccessToken](../api/model.md#Model+getAccessToken).
+[verifyScope](../api/model.md#Model+verifyScope) is required only when
+`options.scope` configures scope checking. A custom `authenticateHandler` does
+not replace `OAuth2Server.authenticate`.
## Grant Types
-For each [grant type](./grant-types.md) there are different model required, optional or unused.
-The following sections describe the grant types and the model functions, required for
-the grants.
+Required hooks must be provided even when a particular request does not reach
+them. Optional generators have built-in defaults: 32 random bytes encoded as
+64 lowercase hexadecimal characters. Scope validation is optional; without a
+validator, requested scopes are accepted.
### Authorization Code Grant
-Model functions required by the [authorization code grant](./grant-types.md#authorization-code-grant-type):
+Authorization endpoint requirements:
-- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code)
-- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode)
-- [getAuthorizationCode](../api/model.md#modelgetauthorizationcodeauthorizationcode--code-promiseauthorizationcodedata-code)
-- [saveAuthorizationCode](../api/model.md#modelsaveauthorizationcodecode-client-user--codepromiseobjectcode)
-- [revokeAuthorizationCode](../api/model.md#modelrevokeauthorizationcodecode--codepromisebooleancode)
+- Required: [getClient](../api/model.md#Model+getClient) and
+ [saveAuthorizationCode](../api/model.md#Model+saveAuthorizationCode).
+- Conditional: without a custom `authenticateHandler`, the built-in bearer
+ authentication requires `getAccessToken`; scoped authentication also requires
+ `verifyScope` and explicit `addAcceptedScopesHeader` and
+ `addAuthorizedScopesHeader` options (each may be true or false).
+- Optional: [generateAuthorizationCode](../api/model.md#Model+generateAuthorizationCode),
+ [validateScope](../api/model.md#Model+validateScope), and
+ [validateRedirectUri](../api/model.md#Model+validateRedirectUri).
+ Registered `client.redirectUris` are required; the default validator checks exact inclusion.
-Required if no custom authenticate handler used:
-- [verifyScope](../api/model.md#modelverifyscopeaccesstoken-scope--codepromisebooleancode)
+For session-based login, supply a handler returning the logged-in user. Handle
+login before calling `authorize`: a falsy user causes a `ServerError` (503),
+not a login challenge. The bearer fallback uses the authenticated token's user.
-Required if custom authenticate handler used:
-- [getAccessToken](../api/model.md#modelgetaccesstokenaccesstoken--code-promiseaccesstokendata-code)
+Code exchange requirements:
-**Optional, but recommended:**
-- [generateAccessToken](../api/model.md#modelgenerateaccesstokenclient-user-scope--codepromisestringcode)
-- [generateRefreshToken](../api/model.md#modelgeneraterefreshtokenclient-user-scope--codepromisestringcode)
-- [generateAuthorizationCode](../api/model.md#modelgenerateauthorizationcodeclient-user-scope--codepromisestringcode)
-- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode)
-- [validateRedirectUri](../api/model.md#modelvalidateredirecturiredirecturi-client--codepromisebooleancode)
+- Required: `getClient`, [getAuthorizationCode](../api/model.md#Model+getAuthorizationCode),
+ [revokeAuthorizationCode](../api/model.md#Model+revokeAuthorizationCode), and
+ [saveToken](../api/model.md#Model+saveToken).
+- Optional: `generateAccessToken`, `generateRefreshToken`, and `validateScope`.
-### Client Credentials Grant
-
-Model functions used by the [client credentials grant](grant-types.md#client-credentials-grant-type):
+Preserve `codeChallenge` and `codeChallengeMethod` when saving and loading codes.
+See [PKCE](./pkce.md) for endpoint-specific client authentication.
-- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code)
-- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode)
-- [getUserFromClient](../api/model.md#modelgetuserfromclientclient--codepromiseobjectcode)
+### Client Credentials Grant
-**Optional, but recommended:**
-- [generateAccessToken](../api/model.md#modelgenerateaccesstokenclient-user-scope--codepromisestringcode)
-- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode)
+Required: `getClient`, `saveToken`, and
+[getUserFromClient](../api/model.md#Model+getUserFromClient).
+Optional: `generateAccessToken` and `validateScope`. No refresh token is issued.
### Refresh Token Grant
-Model functions used by the [refresh token grant](grant-types.md#refresh-token-grant-type):
-
-- [generateRefreshToken](../api/model.md#modelgeneraterefreshtokenclient-user-scope--codepromisestringcode)
-- [getRefreshToken](../api/model.md#modelgetrefreshtokenrefreshtoken--code-promiserefreshtokendata-code)
-- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code)
-- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode)
-- [revokeToken](../api/model.md#modelrevoketokentoken--codepromisebooleancode)
+Required: `getClient`, `saveToken`,
+[getRefreshToken](../api/model.md#Model+getRefreshToken), and
+[revokeToken](../api/model.md#Model+revokeToken).
+The constructor requires `revokeToken` even with
+`alwaysIssueNewRefreshToken: false`, although it is not called in that mode.
+Optional: `generateAccessToken` and `generateRefreshToken`.
+Scope narrowing is checked against the stored token; `validateScope` is not called.
### Password Grant
-Model functions used by the [password grant](grant-types.md#password-grant-type):
-
-- [generateAccessToken](../api/model.md#modelgenerateaccesstokenclient-user-scope--codepromisestringcode)
-- [generateRefreshToken](../api/model.md#modelgeneraterefreshtokenclient-user-scope--codepromisestringcode)
-- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code)
-- [getUser](../api/model.md#modelgetuserusername-password-client--codepromiseobjectnullundefinedfalse0code)
-- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode)
-- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode)
+Required: `getClient`, [getUser](../api/model.md#Model+getUser), and `saveToken`.
+Optional: `generateAccessToken`, `generateRefreshToken`, and `validateScope`.
+See the [password grant warning](./grant-types.md#password-grant-type).
### Extension Grants
-The authorization server may also implement custom grant types to issue access (and optionally refresh) tokens.
-
-See [extension grants](./grant-types.md#extension-grants)
-
+See [extension grants](./grant-types.md#extension-grants) for custom hook requirements.
diff --git a/docs/guide/pkce.md b/docs/guide/pkce.md
index 206b59c2..12c8117e 100644
--- a/docs/guide/pkce.md
+++ b/docs/guide/pkce.md
@@ -3,7 +3,7 @@
Starting with release [4.3.0](https://github.com/node-oauth/node-oauth2-server/releases/tag/v4.3.0) this library
supports PKCE (Proof Key for Code Exchange by OAuth Public Clients) as defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636).
-The PKCE integrates only with the `authorization code `. The abstract workflow looks like
+The PKCE integrates only with the [authorization code grant](./grant-types.md#authorization-code-grant-type). The abstract workflow looks like
the following:
```
@@ -70,9 +70,9 @@ strong candidate to become the default in a future major release.
-The following shows an example of how a client could generate a code_challenge and
-code_challenge_method\` for the authorizazion request.
+The client generates `code_challenge` and `code_challenge_method` for the authorization request:
+ const crypto = require('node:crypto')
const base64URLEncode = str => str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
@@ -95,26 +95,41 @@ The following shows an example of how a client could generate a fs.readFileSync(path.join(root, p), 'utf8');
+let passed = 0;
+async function check(name, fn) {
+ await fn();
+ console.log(`PASS ${++passed}: ${name}`);
+}
+
+function example(name, db) {
+ const block = read('lib/model.js').match(new RegExp(' \\* (?:async )?function ' + name + '\\([\\s\\S]*?\\n\\s*\\*/'))[0];
+ const source = block.replace(/\n\s*\*\/$/, '').replace(/^\s*\* ?/gm, '');
+ return new Function('db', `${source}\nreturn ${name};`)(db);
+}
+function fenced(file) {
+ return [...read(file).matchAll(/```\s*js\n([\s\S]*?)```/g)].map(m => m[1]);
+}
+function frameworkResponse() {
+ return {
+ headers: {}, statusCode: 200,
+ set(headers) { Object.assign(this.headers, headers); return this; },
+ status(code) { this.statusCode = code; return this; },
+ json(body) { this.body = body; return this; },
+ end() { this.ended = true; return this; }
+ };
+}
+const user = { id: 'user' };
+const client = { id: 'client', grants: ['authorization_code', 'refresh_token', 'client_credentials'], redirectUris: ['https://client.example/callback'] };
+const future = () => new Date(Date.now() + 60000);
+const tokenRequest = body => new Request({ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', 'content-length': '100' }, query: {}, body });
+const authRequest = overrides => ({ method: 'GET', headers: {}, query: { client_id: client.id, response_type: 'code', state: 'state-123', ...overrides }, session: { user } });
+
+async function main() {
+ const records = {};
+ const db = {
+ async saveAccessToken(row) { records.access = row; return row; },
+ async saveRefreshToken(row) { records.refresh = row; return row; },
+ async saveAuthorizationCode(row) { records.code = row; return row; },
+ async queryAccessToken({ access_token }) { return records.access?.access_token === access_token ? records.access : null; },
+ async queryRefreshToken({ refresh_token }) { return records.refresh?.refresh_token === refresh_token ? records.refresh : null; },
+ async queryAuthorizationCode({ authorization_code }) { return records.code?.authorization_code === authorization_code ? records.code : null; },
+ async queryClient() { return client; },
+ async queryUser() { return user; }
+ };
+ const snippets = Object.fromEntries(['saveToken', 'getAccessToken', 'getRefreshToken', 'saveAuthorizationCode', 'getAuthorizationCode'].map(name => [name, example(name, db)]));
+
+ await check('native Promise save/load examples, with and without refresh fields', async () => {
+ const access = { accessToken: 'access', accessTokenExpiresAt: future(), scope: ['read'] };
+ assert.deepEqual(await snippets.saveToken(access, client, user), { ...access, client, user });
+ assert.equal(records.refresh, undefined);
+ assert.deepEqual(await snippets.getAccessToken('access'), { ...access, client, user });
+ const token = { ...access, refreshToken: 'refresh', refreshTokenExpiresAt: future() };
+ await snippets.saveToken(token, client, user);
+ assert.deepEqual(await snippets.getRefreshToken('refresh'), { refreshToken: 'refresh', refreshTokenExpiresAt: token.refreshTokenExpiresAt, scope: ['read'], client, user });
+ for (const name of ['getAccessToken', 'getRefreshToken', 'getAuthorizationCode']) assert.equal(await snippets[name]('missing'), false);
+ });
+
+ const verifier = 'a'.repeat(43);
+ const pkce = require('../../lib/pkce/pkce');
+ const challenge = pkce.getHashForCodeChallenge({ method: 'S256', verifier });
+ const model = { ...snippets, getClient: async () => client, revokeAuthorizationCode: async () => { delete records.code; return true; } };
+ const oauth = new OAuth2Server({ model });
+ const code = { authorizationCode: 'code', expiresAt: future(), redirectUri: client.redirectUris[0], scope: ['read'], codeChallenge: challenge, codeChallengeMethod: 'S256' };
+ await check('documented code mappings retain PKCE fields and matching verifier exchanges', async () => {
+ const saved = await snippets.saveAuthorizationCode(code, client, user);
+ assert.equal(saved.codeChallenge, challenge);
+ assert.equal(saved.codeChallengeMethod, 'S256');
+ assert.deepEqual(await snippets.getAuthorizationCode('code'), { ...code, client, user });
+ const response = new Response();
+ await oauth.token(tokenRequest({ grant_type: 'authorization_code', client_id: client.id, code: 'code', code_verifier: verifier, redirect_uri: code.redirectUri }), response);
+ assert.equal(response.body.scope, 'read');
+ assert.equal(response.body.token_type, 'Bearer');
+ assert.equal(records.code, undefined);
+ });
+ await check('failed PKCE consumes the code', async () => {
+ await snippets.saveAuthorizationCode(code, client, user);
+ await assert.rejects(oauth.token(tokenRequest({ grant_type: 'authorization_code', client_id: client.id, code: 'code', code_verifier: 'b'.repeat(43), redirect_uri: code.redirectUri }), new Response()), { name: 'invalid_grant' });
+ assert.equal(records.code, undefined);
+ });
+
+ const customSource = fenced('docs/guide/grant-types.md')[0];
+ const module = { exports: {} };
+ new Function('require', 'module', customSource)(() => OAuth2Server, module);
+ const Custom = module.exports;
+ let saves = 0;
+ let generatorScopes = [];
+ const customModel = {
+ getClient: async () => ({ ...client, grants: ['urn:example:custom'] }),
+ validateScope: async () => ['read'],
+ generateAccessToken: async (_c, _u, scope) => { generatorScopes.push(scope); return 'access'; },
+ generateRefreshToken: async (_c, _u, scope) => { generatorScopes.push(scope); return 'refresh'; },
+ saveToken: async (token, client, user) => { saves++; return { ...token, client, user }; }
+ };
+ const custom = new Custom({ model: customModel, accessTokenLifetime: 60, refreshTokenLifetime: 60 });
+ await check('extension snippet awaits and uses reduced scope', async () => {
+ const token = await custom.saveToken(user, client, ['read', 'write']);
+ assert.deepEqual(token.scope, ['read']);
+ assert.deepEqual(generatorScopes, [['read'], ['read']]);
+ });
+ await check('async scope rejection prevents token generation and saving', async () => {
+ customModel.validateScope = async () => { await Promise.resolve(); return false; };
+ await assert.rejects(custom.saveToken(user, client, ['write']), { name: 'invalid_scope' });
+ assert.equal(saves, 1);
+ assert.equal(generatorScopes.length, 2);
+ });
+ await check('extension grant requires client approval and returns narrowed wire scope', async () => {
+ Custom.prototype.getUserBySomething = async () => user;
+ customModel.validateScope = async () => ['read'];
+ const server = new OAuth2Server({ model: customModel, extendedGrantTypes: { 'urn:example:custom': Custom } });
+ const request = tokenRequest({ grant_type: 'urn:example:custom', client_id: 'client', client_secret: 'secret', scope: 'read write' });
+ const response = new Response();
+ await server.token(request, response);
+ assert.equal(response.body.scope, 'read');
+ customModel.getClient = async () => client;
+ await assert.rejects(server.token(request, new Response()), { name: 'unauthorized_client' });
+ });
+
+ const routes = {};
+ const context = { OAuth2Server, Request, Response, oauth, app: { get: (route, fn) => { routes[route] = fn; } } };
+ vm.createContext(context);
+ vm.runInContext(fenced('docs/guide/getting-started.md')[1], context);
+ await check('authorization route forwards denial redirect and state', async () => {
+ const res = frameworkResponse();
+ await routes['/authorize'](authRequest({ allowed: 'false' }), res);
+ assert.equal(res.statusCode, 302);
+ const location = new URL(res.headers.location);
+ assert.equal(location.searchParams.get('error'), 'access_denied');
+ assert.equal(location.searchParams.get('state'), 'state-123');
+ });
+ await check('authorization route sends early invalid-client failure locally', async () => {
+ model.getClient = async () => false;
+ const res = frameworkResponse();
+ await routes['/authorize'](authRequest(), res);
+ assert.equal(res.statusCode, 400);
+ assert.equal(res.body.error, 'invalid_client');
+ assert.equal(res.headers.location, undefined);
+ model.getClient = async () => client;
+ });
+ await check('local error helper hides arbitrary exception messages', () => {
+ const res = frameworkResponse();
+ context.sendLocalError(res, new Error('private storage details'), new Response());
+ assert.equal(res.statusCode, 500);
+ assert.equal(res.body.error_description, 'Internal server error');
+ });
+ await check('early token and authentication failures do not become HTTP 200', async () => {
+ const response = new Response();
+ await assert.rejects(oauth.token(new Request(authRequest()), response), { name: 'invalid_request' });
+ assert.equal(response.status, 200);
+ let res = frameworkResponse();
+ context.sendLocalError(res, new OAuth2Server.InvalidRequestError('method'), response);
+ assert.equal(res.statusCode, 400);
+ try { await oauth.authenticate(new Request(authRequest()), response); } catch (err) {
+ res = frameworkResponse();
+ context.sendLocalError(res, err, response);
+ }
+ assert.equal(res.statusCode, 401);
+ assert.match(res.headers['www-authenticate'], /Bearer/);
+ });
+ await check('falsy custom user produces 503 before redirect', async () => {
+ const response = new Response();
+ await assert.rejects(oauth.authorize(new Request(authRequest()), response, { authenticateHandler: { handle: () => false } }), { name: 'server_error', code: 503 });
+ assert.equal(response.get('location'), undefined);
+ });
+
+ const wrappers = fenced('docs/guide/pkce.md').find(s => s.includes('function modelForEndpoint'));
+ const servers = new Function('OAuth2Server', 'storageModel', 'findClient', 'verifyClientSecret', `${wrappers}\nreturn { authorizationServer, tokenServer };`)(OAuth2Server, model, async id => id === client.id ? { ...client, confidential: true } : false, async (_client, secret) => secret === 'correct');
+ await check('endpoint-specific lookup distinguishes missing and supplied secrets', async () => {
+ assert.ok(await servers.authorizationServer.options.model.getClient(client.id, null));
+ for (const secret of [undefined, null, '', 'wrong']) assert.equal(await servers.tokenServer.options.model.getClient(client.id, secret), false);
+ assert.ok(await servers.tokenServer.options.model.getClient(client.id, 'correct'));
+ const response = new Response();
+ await servers.authorizationServer.authorize(new Request(authRequest()), response, { authenticateHandler: { handle: () => user } });
+ assert.equal(response.status, 302);
+ for (const secret of [undefined, 'wrong']) {
+ await assert.rejects(servers.tokenServer.token(tokenRequest({ grant_type: 'authorization_code', client_id: client.id, client_secret: secret, code_verifier: verifier, code: 'code' }), new Response()), { name: 'invalid_client' });
+ }
+ });
+ await check('scope wire boundary, helper return values and 64-character default', async () => {
+ const { parseScope } = require('../../lib/utils/scope-util');
+ assert.deepEqual(parseScope('read write'), ['read', 'write']);
+ assert.equal(parseScope(null), undefined);
+ assert.throws(() => parseScope(['read']), { name: 'invalid_scope' });
+ assert.equal(tokenRequest({}).is('application/x-www-form-urlencoded'), 'application/x-www-form-urlencoded');
+ assert.equal(tokenRequest({}).get('missing'), undefined);
+ assert.match(await require('../../lib/utils/token-util').generateRandomToken(), /^[0-9a-f]{64}$/);
+ });
+ await check('per-grant map works while global false leaves authentication enabled', () => {
+ const Handler = require('../../lib/handlers/token-handler');
+ const opts = { model, accessTokenLifetime: 60, refreshTokenLifetime: 60 };
+ assert.equal(new Handler({ ...opts, requireClientAuthentication: false }).isClientAuthenticationRequired('refresh_token'), true);
+ assert.equal(new Handler({ ...opts, requireClientAuthentication: { refresh_token: false } }).isClientAuthenticationRequired('refresh_token'), false);
+ });
+ await check('plain PKCE gating and omitted-method defaults on both endpoints', () => {
+ const Authorize = require('../../lib/handlers/authorize-handler');
+ const Grant = require('../../lib/grant-types/authorization-code-grant-type');
+ for (const enabled of [false, true]) {
+ const options = { model, accessTokenLifetime: 60, authorizationCodeLifetime: 60, enablePlainPKCE: enabled, authenticateHandler: { handle: () => user } };
+ const authorize = new Authorize(options);
+ const grant = new Grant(options);
+ assert.equal(authorize.getCodeChallengeMethod(new Request(authRequest())), enabled ? 'plain' : 'S256');
+ assert.equal(grant.getCodeChallengeMethod(), enabled ? 'plain' : 'S256');
+ if (!enabled) assert.throws(() => authorize.getCodeChallengeMethod(new Request(authRequest({ code_challenge_method: 'plain' }))));
+ }
+ assert.equal(pkce.isValidMethod('sha256'), false);
+ });
+ await check('disabled refresh rotation still requires revoke hook and omits new refresh fields', async () => {
+ const Grant = require('../../lib/grant-types/refresh-token-grant-type');
+ const options = { model: { getRefreshToken: () => ({ refreshToken: 'old', scope: ['read', 'write'], client, user }), saveToken: snippets.saveToken }, accessTokenLifetime: 60, refreshTokenLifetime: 60, alwaysIssueNewRefreshToken: false };
+ assert.throws(() => new Grant(options), { name: 'invalid_argument' });
+ options.model.revokeToken = () => { throw Error('must not revoke'); };
+ const token = await new Grant(options).handle(tokenRequest({ refresh_token: 'old', scope: 'read' }), client);
+ assert.equal(token.refreshToken, undefined);
+ assert.deepEqual(token.scope, ['read']);
+ });
+
+ await check('generated server token example uses the resolved token', async () => {
+ const source = fenced('docs/api/server.md').find(s => s.includes('function tokenHandler'));
+ const factory = new Function('OAuth2Server', 'require', `${source}\nreturn tokenHandler;`)(OAuth2Server, () => ({ ...model, getUserFromClient: () => user }));
+ const res = { ...frameworkResponse(), locals: {} };
+ let continued = false;
+ await factory({})(tokenRequest({ grant_type: 'client_credentials', client_id: client.id, client_secret: 'secret' }), res, () => { continued = true; });
+ assert.equal(continued, true);
+ assert.ok(res.locals.oauth.token.accessToken);
+ });
+ await check('PKCE guide endpoint sketches run with their documented prerequisites', async () => {
+ const endpoints = {};
+ const ctx = {
+ require: () => OAuth2Server,
+ model,
+ app: { get: (p, fn) => { endpoints[p] = fn; }, post: (p, fn) => { endpoints[p] = fn; } },
+ sendLocalError: context.sendLocalError
+ };
+ vm.createContext(ctx);
+ for (const source of fenced('docs/guide/pkce.md').filter(s => s.includes("app.get('/authorize'") || s.includes("app.post('/token'"))) vm.runInContext(source, ctx);
+ const authRes = frameworkResponse();
+ await endpoints['/authorize'](authRequest({ code_challenge: challenge, code_challenge_method: 'S256', scope: 'read' }), authRes);
+ assert.equal(authRes.statusCode, 302);
+ const codeValue = new URL(authRes.headers.location).searchParams.get('code');
+ const res = frameworkResponse();
+ await endpoints['/token'](tokenRequest({ grant_type: 'authorization_code', client_id: client.id, code: codeValue, code_verifier: verifier, redirect_uri: client.redirectUris[0] }), res);
+ assert.equal(res.statusCode, 200);
+ assert.equal(res.body.scope, 'read');
+ assert.ok(res.body.access_token);
+ });
+ await check('early and caught programming errors follow the documented normalization boundaries', async () => {
+ const failure = new OAuth2Server.InvalidArgumentError('fixture');
+ const options = { authenticateHandler: { handle: () => user } };
+ const early = new OAuth2Server({ model: { ...model, getClient: () => { throw failure; } } });
+ await assert.rejects(early.authorize(new Request(authRequest()), new Response(), options), err => err === failure);
+ const caught = new OAuth2Server({ model: { ...model, saveAuthorizationCode: () => { throw failure; } } });
+ await assert.rejects(caught.authorize(new Request(authRequest()), new Response(), options), err => err.name === 'server_error' && err.inner === failure);
+ const authenticate = new OAuth2Server({ model: { getAccessToken: () => { throw failure; } } });
+ const request = new Request({ method: 'GET', query: {}, headers: { authorization: 'Bearer token' } });
+ await assert.rejects(authenticate.authenticate(request, new Response()), err => err === failure);
+ let ran = false;
+ const generator = new OAuth2Server({ model: { *getAccessToken() { ran = true; return { user, accessTokenExpiresAt: future() }; } } });
+ await assert.rejects(generator.authenticate(request, new Response()), { name: 'server_error' });
+ assert.equal(ran, false);
+ });
+
+ await check('all changed library and generator executable ASTs match HEAD', () => {
+ const files = execFileSync('git', ['diff', '--name-only', '--', 'lib', 'docs/build-api.js'], { cwd: root, encoding: 'utf8' }).trim().split('\n').filter(Boolean);
+ function ast(source) {
+ return JSON.stringify(parse(source), (key, value) => ['start', 'end', 'loc', 'comments', 'leadingComments', 'trailingComments', 'innerComments', 'extra'].includes(key) ? undefined : value);
+ }
+ for (const file of files) assert.equal(ast(read(file)), ast(execFileSync('git', ['show', `HEAD:${file}`], { cwd: root, encoding: 'utf8' })), file);
+ });
+ await check('guide JavaScript fences and repaired model examples parse', () => {
+ for (const file of ['getting-started', 'pkce', 'grant-types']) for (const source of fenced(`docs/guide/${file}.md`)) parse(source);
+ for (const file of ['server', 'request', 'response']) for (const source of fenced(`docs/api/${file}.md`)) parse(source);
+ for (const block of read('lib/model.js').matchAll(/@example\n([\s\S]*?)(?=\n\s*\* @example|\n\s*\*\/)/g)) parse(block[1].replace(/^\s*\* ?/gm, ''));
+ });
+ await check('published HTML links resolve to pages and real rendered fragments', () => {
+ const dist = path.join(root, 'docs/.vitepress/dist');
+ const htmlFiles = fs.readdirSync(dist, { recursive: true }).filter(p => p.endsWith('.html'));
+ const cache = new Map(htmlFiles.map(p => [p, fs.readFileSync(path.join(dist, p), 'utf8')]));
+ let checked = 0;
+ const failures = new Set();
+ for (const [file, html] of cache) {
+ for (const [, raw] of html.matchAll(/href="([^"]+)"/g)) {
+ if (/^(https?:|mailto:|data:)/.test(raw)) continue;
+ const url = new URL(raw.replaceAll('&', '&'), `https://docs.example/node-oauth2-server/${file}`);
+ let target = decodeURIComponent(url.pathname).replace(/^\/node-oauth2-server\//, '');
+ if (!target) target = 'index.html';
+ if (target.endsWith('/')) target += 'index.html';
+ if (!path.extname(target)) target += '.html';
+ if (!target.endsWith('.html')) continue;
+ const content = cache.get(target);
+ if (!content) { failures.add(`${file}: missing page ${raw}`); continue; }
+ const fragment = decodeURIComponent(url.hash.slice(1));
+ if (fragment && !content.includes(`id="${fragment}"`) && !content.includes(`name="${fragment}"`)) failures.add(`${file}: missing fragment ${raw}`);
+ checked++;
+ }
+ }
+ assert.deepEqual([...failures], []);
+ assert.ok(!htmlFiles.some(p => p.startsWith('plans/')));
+ console.log(` ${checked} local page/fragment links checked; planning files excluded`);
+ });
+ console.log(`${passed} documentation checks passed.`);
+}
+
+main().catch(error => { console.error(error); process.exitCode = 1; });
diff --git a/docs/plans/documentation-consistency-implementation-results.md b/docs/plans/documentation-consistency-implementation-results.md
new file mode 100644
index 00000000..874b1d84
--- /dev/null
+++ b/docs/plans/documentation-consistency-implementation-results.md
@@ -0,0 +1,103 @@
+# Documentation consistency implementation results and history
+
+Implemented on 2026-10-14 against `c57d565` (package version `5.3.1`).
+Source plan: [documentation-consistency-update-plan.md](./documentation-consistency-update-plan.md).
+
+All 17 plan items are implemented. Changes are uncommitted. The original plan
+was preserved, including its pre-existing untracked status. No library behavior,
+package exports, dependency declarations, or TypeScript contracts were changed.
+
+## Results by plan item
+
+| Item | Implementation result |
+| --- | --- |
+| 1. Scope boundary | Migration and token guides distinguish HTTP strings from model arrays, reject HTTP arrays, and explain omitted scope. Grant JSDoc now describes arrays and asynchronous validation. Authentication options accept strings or arrays. |
+| 2. Extension grant | The example awaits validation and uses the returned scope for both generators and storage. Documentation makes `client.grants` approval mandatory. |
+| 3. Response handling | Getting-started and PKCE authorization examples forward prepared redirects after rejection. Early failures use a local error response with fixed public descriptions. Authentication, token, and authorization response responsibilities are separated; adapters copy fields to the framework response. |
+| 4. Model requirements | The model guide separates authorization from code exchange and required, conditional, and optional hooks. It explains bearer fallback, scoped authentication options, and the required refresh revocation hook even when rotation is disabled. Custom authentication bypass is limited to authorization. |
+| 5. Native Promise examples | Model examples use native Promise destructuring and async/await. Refresh storage is conditional; lookup examples handle absent records. The server token example uses its resolved `token` variable. |
+| 6. PKCE storage | Authorization-code types, parameters, and save/load mappings preserve `codeChallenge` and `codeChallengeMethod`. |
+| 7. Client authentication | Model/server documentation distinguishes null-secret authorization lookup from token authentication. The PKCE guide supplies separate endpoint model wrappers that validate supplied secrets and reject missing confidential token credentials. The option is documented as a per-grant map. |
+| 8. Signatures | Request/Response constructors and crypto/PKCE object arguments have parent options parameters. `authenticate` has request, response, and options parameters. Published handler/grant methods have their missing inputs documented. Request helper return types and wrapper examples are corrected. |
+| 9. Scope validation result | `validateScope` documents an accepted array or falsy rejection, permits synchronous values, handles omitted scope in examples, and distinguishes `verifyScope`'s boolean result. |
+| 10. Generators | Current guidance supports synchronous values, native Promises, and async functions. Direct generator hooks are explicitly unsupported without an application-provided runner. |
+| 11. Random defaults | All three default generators document 32 random bytes encoded as 64 lowercase hexadecimal characters. |
+| 12. PKCE workflow | Documentation uses S256, explains plain gating and omitted-method defaults, defines endpoint prerequisites/wrappers/options, and explains that failed verification after revocation consumes the code. Existing `requirePKCE` and refresh explanations are retained. |
+| 13. Error boundaries | Error and server JSDoc distinguish early failures from normalization inside catch blocks, explain the 503 falsy-user result, and use `allowed` in body or query. |
+| 14. Compliance boundaries | The compliance guide ties validation guarantees and parameter sources to individual endpoints. It distinguishes exact registered-redirect matching from the optional override, conditional refresh expiry, access-token authentication checks, and model-owned atomic revocation. |
+| 15. Navigation | Fixed grant model targets, extension-grant RFC section target, legacy link notation, homepage API action, and cross-page API anchors. Added rendered anchor compatibility in VitePress after verification exposed fragment normalization mismatches. |
+| 16. Token/reference coverage | The token guide describes credential fields, expiry, wire representation, lifecycle, refresh rotation and narrowing, and optional refresh expiry. Unsupported MAC/implicit classes are labeled in source JSDoc. Public exports and internal references, including `Model.from`, are distinguished. |
+| 17. Contributor tooling | `CONTRIBUTING.md` uses `docs:api`; generator usage comments now name the actual script and defaults. The contributing-page symlink remains intact. |
+
+All affected API Markdown was regenerated from source JSDoc, not patched directly.
+
+## Implementation history
+
+1. Read the plan, package scripts, guides, source comments, exports, and relevant
+ handler/grant implementations. Confirmed the working repository was the nested
+ `node-oauth2-server` checkout and that its existing untracked content was the
+ planning directory.
+2. Updated the integration examples and model contracts together. Filled the
+ token guide and repaired the remaining signatures, defaults, and behavior
+ descriptions in source comments.
+3. Regenerated the API and built the site. Inspection caught a JSDoc parsing
+ issue where descriptive text after `@constructor` became a class name; moved
+ that text before the tag and regenerated the corrected signatures.
+4. Added reproducible, in-memory checks that extract and run the actual examples.
+ Checks covered reduced/rejected scopes, native Promises, PKCE storage/exchange,
+ consumed codes, redirects and local failures, client authentication, refresh
+ rotation, generator rejection, and error normalization.
+5. Audited rendered HTML links. VitePress normalized fragments such as
+ `Model+getClient` to `model-getclient`, while JSDoc's raw named anchors retained
+ their original spelling. Added a Markdown renderer hook that supplies matching
+ IDs for JSDoc anchors while retaining the original names. The first renderer
+ attempt matched a complete anchor; inspection showed inline opening and closing
+ tags are separate tokens, so the final hook matches the opening tag.
+6. Explicitly excluded `plans/**` from published pages. The plan, this history,
+ and the verification script remain repository artifacts outside public navigation.
+7. Ran the existing tests, completed the final API/site build and acceptance
+ checks, and compared parsed executable syntax against HEAD to confirm all
+ library and generator edits are comment-only. No commit was created.
+
+## Verification
+
+Commands run from the repository root:
+
+```sh
+npm test
+npm run docs:build
+node docs/plans/documentation-consistency-checks.cjs
+git diff --check -- . ':!docs/api'
+```
+
+- `npm test`: passed its Biome pretest check and all **464 tests**.
+- `npm run docs:build`: regenerated the API, maintained the contributing symlink,
+ and successfully built/rendered the site.
+- [documentation-consistency-checks.cjs](./documentation-consistency-checks.cjs):
+ **22 checks passed**, including actual guide and generated API examples,
+ unchanged executable ASTs for modified library files and the API generator,
+ and **2,991 local rendered page/fragment links**. No planning pages were published.
+- Hand-edited tracked files passed the whitespace diff check. A full
+ `git diff --check` reports the generator's existing Markdown trailing-space and
+ blank-line conventions in regenerated output; these were retained so generated
+ files remain reproducible from JSDoc.
+- Final inspection confirmed corrected generated constructor/method signatures,
+ removal of `.spread()` and obsolete documentation claims in the targeted files,
+ and preservation of `docs/guide/contributing.md -> ../../CONTRIBUTING.md`.
+
+The initial sandboxed generation/check runs could not complete subprocess calls
+(`spawnSync ... EPERM`). They were rerun successfully with subprocess permissions.
+Build output also included the existing npm `min-release-age` warning and JSDoc
+module-tag warnings; runtime checks emitted Node's existing `url.parse()`
+deprecation warning. None prevented final validation.
+
+## Scope limits and deferred behavior changes
+
+This implements documentation corrections, documentation rendering/navigation,
+and verification only. No new standards claims or external adapter-maintenance
+claims were independently verified, and external links were not audited.
+
+Potential runtime enhancements remain separate work: endpoint/client-type context
+in `getClient`, login-challenge behavior for a falsy custom user, broader error
+normalization, or altered response preparation. The documentation describes the
+current behavior without promising those changes.
diff --git a/docs/plans/documentation-consistency-update-plan.md b/docs/plans/documentation-consistency-update-plan.md
new file mode 100644
index 00000000..05187615
--- /dev/null
+++ b/docs/plans/documentation-consistency-update-plan.md
@@ -0,0 +1,159 @@
+# Documentation consistency update plan
+
+Reviewed on 2026-10-13 against local commit `c57d565` (package version `5.3.1`).
+
+## Scope and approach
+
+This is a plan only. No documentation corrections, implementation changes, regenerated API pages, or commits were made during this review.
+
+Reviewed the guides, generated API pages, documentation navigation and generation script against `lib/`, `index.js`, `index.d.ts`, and package scripts. Findings describe the local implementation; this is not an independent standards compliance audit or a verification of external adapter maintenance. Paths below are relative to the repository root. Source symbols identify the relevant implementation even if line numbers move.
+
+The API pages are generated from JSDoc by `docs/build-api.js`. Correct their source comments and regenerate them during implementation; do not fix generated Markdown alone. `docs/guide/contributing.md` is a symlink to `CONTRIBUTING.md`, so its source must be updated there.
+
+## Priority 1: examples and contracts that can break integrations
+
+### 1. Distinguish HTTP scope strings from internal scope arrays
+
+- **Documentation:** `docs/guide/migrating-to-v5.md`, โScope is now Arrayโ, says requests, responses, and models all require arrays. `docs/api/grant-types/abstract-grant-type.md` instead documents string results for `getScope` and `validateScope`; `docs/api/grant-types/authorization-code-grant-type.md` documents `requestedScope` as a string.
+- **Evidence:** `lib/utils/scope-util.js:parseScope` accepts strings/null/undefined and rejects arrays. Authorization and grant handlers parse HTTP scope strings into arrays. `lib/handlers/token-handler.js:updateSuccessResponse` joins arrays into the HTTP response string. `AuthenticateHandler` separately accepts a string or array for the JavaScript `options.scope` setting.
+- **Proposed update:** Explain the boundary with `scope=read write` on the wire, `['read', 'write']` in model hooks, and `scope: 'read write'` in token JSON. Correct grant JSDoc argument and return types, including the Promise return from `validateScope`, and document optional scope values.
+- **Acceptance:** Examples use strings in HTTP requests, arrays in models, and strings in HTTP token responses; passing an HTTP array is explicitly unsupported.
+
+### 2. Await and use validated scopes in the extension grant example
+
+- **Documentation:** `docs/guide/grant-types.md`, `MyCustomGrantType.saveToken`, calls `this.validateScope(user, client, scope)` without awaiting or using its result.
+- **Evidence:** `lib/grant-types/abstract-grant-type.js:validateScope` is async and returns the model's validated scope or rejects. Built-in password and client credentials grants await it and use the returned scope when generating and saving tokens.
+- **Proposed update:** Use `const validatedScope = await this.validateScope(...)` and pass that value to both generators and `model.saveToken`. Explain that the extension must also appear in `client.grants`: `TokenHandler.handleGrantType` enforces this unconditionally, whereas the guide currently makes approval sound conditional on custom checks.
+- **Acceptance:** An asynchronously rejected scope prevents saving a token, and a reduced scope is the one persisted and returned. An unapproved extension grant is documented as rejected.
+
+### 3. Correct authorization error handling and response responsibilities
+
+- **Documentation:** `docs/guide/getting-started.md`, โSending the responseโ, returns JSON from every authorization catch block. It also says all three server methods populate status, headers, and body. `docs/guide/compliance.md` broadly promises error bodies and response headers.
+- **Evidence:** `AuthorizeHandler.handle` sets a 302 error redirect and then throws for errors inside its redirect-capable block. The guide's catch discards that redirect. Earlier client/user failures can reject without a redirect. `AuthenticateHandler.handle` sets selected headers but does not populate an error status/body; token method/content-type checks occur before its response-producing catch. Token cache headers are set on success, not by `updateErrorResponse`.
+- **Proposed update:** Show authorization handling that forwards a prepared redirect on success or failure, and uses an appropriate local error response when no redirect was prepared. Explain each endpoint's response contract and the integration's responsibility to handle rejections and copy headers. Do not expose arbitrary internal exceptions through a blanket JSON error handler. Change the adapter guide's instruction to copy fields onto the framework **response**, not its request.
+- **Acceptance:** An `allowed=false` request sends the prepared error redirect including state; invalid-client errors without a prepared redirect are handled locally. Authentication and early token failures do not accidentally send the wrapper's default 200 response.
+
+### 4. Repair the model requirements and authentication fallback guidance
+
+- **Documentation:** `docs/guide/model.md` says `getAccessToken` is required when a custom authenticate handler is used and lists `verifyScope` as required without a custom handler. It mixes optional generators with required hooks for password/refresh grants. `docs/guide/compliance.md` says a custom handler must be supplied. `docs/api/model.md:verifyScope` says the hook is never called when an `authenticateHandler` option exists.
+- **Evidence:** `AuthorizeHandler` falls back to `new AuthenticateHandler(options)` only when a custom handler is absent. That fallback needs `getAccessToken`; `verifyScope` is required when authentication scope checking is configured. `OAuth2Server.authenticate` always uses `AuthenticateHandler` regardless of `authenticateHandler`. `AbstractGrantType` supplies default token generators and permits omission of `validateScope`. The refresh constructor requires `revokeToken` even when rotation is disabled, although its invocation is then skipped.
+- **Proposed update:** Separate authorization endpoint requirements from code exchange requirements and classify hooks as required, conditional, or optional. Explain the built-in bearer-token fallback and when a custom user/session handler is appropriate. Limit the custom-handler bypass statement to `authorize`.
+- **Acceptance:** The required-hook lists match each constructor and call path, including scoped authentication and refresh rotation disabled.
+
+### 5. Replace non-working native Promise examples
+
+- **Documentation:** `docs/api/model.md` examples for `saveToken`, `getAccessToken`, `getRefreshToken`, and `getAuthorizationCode` use `.spread()`. The `saveToken` example also has a semicolon before `.spread`. `docs/api/server.md`'s token example names the resolved value `code` but accesses an undefined `token`.
+- **Evidence:** `lib/model.js` and `lib/server.js` contain these source examples. Runtime code uses native async/await; `package.json` has no Promise implementation providing `.spread()`.
+- **Proposed update:** Rewrite source examples with async/await or `.then(([...]) => ...)`, correct the token callback variable, and make optional refresh-token storage conditional so the example works for the client credentials grant as well.
+- **Acceptance:** The snippets parse and execute with native Promises and a small in-memory model, including a token without refresh fields.
+
+### 6. Include PKCE fields in the model contract and storage examples
+
+- **Documentation:** `docs/api/model.md` omits `codeChallenge` and `codeChallengeMethod` from `AuthorizationCodeData`, `saveAuthorizationCode` parameters, and save/load examples. The PKCE guide explicitly requires preserving them.
+- **Evidence:** `AuthorizeHandler.saveAuthorizationCode` supplies both fields; `AuthorizationCodeGrantType.verifyPKCE` consumes them. The existing model examples construct new objects without copying them. `index.d.ts` already includes both fields.
+- **Proposed update:** Add the optional fields to JSDoc data types and persist/restore them in examples. Explain that dropping these fields prevents a PKCE exchange from succeeding.
+- **Acceptance:** A code saved and loaded using the documented mapping retains both PKCE fields and succeeds with the matching verifier.
+
+### 7. Make client authentication guidance match the available context
+
+- **Documentation:** `docs/api/model.md:getClient` and `docs/guide/pkce.md` instruct models to reject confidential clients when a secret is absent, without adequately distinguishing authorization requests from token requests. `docs/api/server.md` also lists `requireClientAuthentication` as `object | boolean`.
+- **Evidence:** `AuthorizeHandler.getClient` always calls `model.getClient(clientId, null)`, including for confidential clients. `TokenHandler.getClientCredentials` preserves a supplied secret and permits its absence for PKCE or configured grants. The same model hook has no explicit endpoint argument. A bare `requireClientAuthentication: false` does not disable the check; a per-grant map does. `index.d.ts` already uses `Record`.
+- **Proposed update:** Explain both `getClient` call contexts, supplied-secret validation, and the limitation of inferring endpoint/client authentication solely from an absent secret. Preserve the requirement to authenticate confidential token requests, but do not suggest a universal absent-secret rejection that breaks authorization. Describe endpoint-aware integration/model separation if presenting a complete enforcement example. Document the option as a per-grant map and explicitly reject the interpretation of a global boolean switch.
+- **Acceptance:** A confidential client can start authorization without sending its secret to that endpoint, while the illustrated token integration rejects missing/wrong confidential credentials. Examples distinguish absent from supplied secrets and use maps for authentication options. Any desired runtime API enhancement is a separate task.
+
+## Priority 2: incorrect API descriptions and missing behavior
+
+### 8. Correct generated public signatures and helper signatures
+
+- **Documentation:** `docs/api/server.md` renders `authenticate()` without arguments. `docs/api/request.md` and `docs/api/response.md` show positional constructor parameters; the latter includes a method parameter that is not a dedicated constructor field. Crypto and PKCE pages also show positional calls for object-argument functions.
+- **Evidence:** `OAuth2Server.authenticate(request, response, options)` takes three arguments. `Request` and `Response` destructure one options object. `createHash`, `getHashForCodeChallenge`, and `isPKCERequest` each take one object. `Request.is` returns a matching MIME string or false, not just a boolean; `Request.get` can return undefined.
+- **Proposed update:** Add parent options-object JSDoc parameters with nested fields, add missing server method parameters, and correct return types. Audit the published handler/grant methods currently rendered as zero-argument methods despite requiring inputs, such as `TokenHandler.handle` and `RefreshTokenGrantType.getRefreshToken`.
+- **Acceptance:** Regenerated signatures agree with callable implementations and include usable invocation examples. Source changes are comment-only.
+
+### 9. Correct the validateScope return type
+
+- **Documentation:** `docs/api/model.md:validateScope` declares `Promise`, while its own examples return arrays.
+- **Evidence:** `AbstractGrantType.validateScope` and `AuthorizeHandler.validateScope` pass truthy results through as scopes. Token serialization expects an array with `.join()`. `index.d.ts` already describes `Promise`.
+- **Proposed update:** Document an accepted array of scopes or a falsy rejection, with sync and async model implementations supported. Explain that returning `true` is not a usable validated scope. Keep `verifyScope`'s boolean contract distinct.
+- **Acceptance:** Model examples return arrays on acceptance and false on rejection; the generated return type agrees with that contract.
+
+### 10. Remove claims of native generator model support
+
+- **Documentation:** `docs/guide/getting-started.md` lists ES6 generators as supported; `docs/api/model.md` says generator model functions are supported.
+- **Evidence:** Runtime hooks are called and awaited directly. Neither `Model.from` nor the handlers execute generator iterators. A generator `getAccessToken` yields an iterator object and fails the required user check.
+- **Proposed update:** Describe synchronous return values, Promises, and async functions as supported model implementations. If mentioning generator-based application code, make explicit that a caller-provided runner must return a supported value/Promise. Preserve older-version behavior only as historical migration context.
+- **Acceptance:** No current-version guidance implies that passing a generator function directly as a model hook works.
+
+### 11. Correct default token/code length and alphabet
+
+- **Documentation:** `docs/api/model.md` says all three default generators produce 40 characters in `a..z0..9`.
+- **Evidence:** `lib/utils/token-util.js:generateRandomToken` uses 32 random bytes encoded as hex; the defaults in `AbstractGrantType` and `AuthorizeHandler` call it. The output is 64 characters in `0..9a..f`.
+- **Proposed update:** Correct all three source descriptions and clarify the bytes-versus-characters distinction in the utility description.
+- **Acceptance:** Access-token, refresh-token, and authorization-code descriptions all match the shared 64-character hexadecimal default.
+
+### 12. Explain PKCE method defaults and repair endpoint sketches
+
+- **Documentation:** `docs/api/pkce/pkce.md:isValidMethod` says `sha256` is an accepted method. `docs/guide/pkce.md` does not explain plain-method gating/defaults, describes body parameters while showing query parameters, and references undefined `response` and `options` variables in endpoint sketches.
+- **Evidence:** `lib/pkce/pkce.js:isValidMethod` accepts `S256` or `plain`, not `sha256`. Both authorization and code exchange disallow plain unless `enablePlainPKCE` is true, defaulting to S256 otherwise. Authorization reads challenge parameters from body or query. Authorization codes are revoked before verifier and redirect verification.
+- **Proposed update:** Correct the algorithm identifier, document `enablePlainPKCE` and omitted-method behavior, instantiate response wrappers and define options/imports in examples, and explain sending the populated response. State that a failed verification consumes the code and a fresh authorization is needed. Preserve the already-correct `requirePKCE` and refresh-token explanations.
+- **Acceptance:** Examples are self-contained or clearly mark prerequisites, use S256, and accurately describe optional PKCE and consumed-code behavior.
+
+### 13. Narrow error normalization and unauthenticated-user descriptions
+
+- **Documentation:** `docs/api/errors/invalid-argument-error.md` broadly says runtime `InvalidArgumentError`s from token/authorize become `ServerError`. `docs/api/server.md` tells a custom authenticate handler to return falsy for a logged-out user without explaining the result, and uses `request.query.allow` in one error description.
+- **Evidence:** `AuthorizeHandler.handle` calls client/user lookup before its normalization catch. An `InvalidArgumentError` from those hooks propagates unchanged; `AuthenticateHandler` also preserves OAuth errors. `AuthorizeHandler.getUser` turns a falsy custom-handler result into `ServerError` (default status 503), not a login challenge. Denial actually uses `allowed` in either body or query.
+- **Proposed update:** Document normalization boundaries and recommend handling login before authorization or explicitly managing the custom-handler error path. Correct `allow` to `allowed` and mention body input too. Record any wish to change error behavior separately rather than promising behavior the code lacks.
+- **Acceptance:** Descriptions cover early versus caught failures and the actual falsy-user behavior; no runtime changes are bundled into the documentation correction.
+
+### 14. Qualify broad compliance and model-responsibility claims
+
+- **Documentation:** `docs/guide/compliance.md` says request parameters are read from both query and body, a custom authenticate handler must be supplied, and redirect validation is a model responsibility; it also broadly groups token existence, ownership, and expiry checks.
+- **Evidence:** Authorization generally supports both locations, but token `grant_type`, credentials, code, verifier, refresh token, and password fields are body/header based; code exchange's redirect URI is an exception. `AuthorizeHandler.validateRedirectUri` defaults to exact inclusion in `client.redirectUris` without a custom model hook. Access-token authentication checks user/expiry but does not independently bind an access token to a requesting client; refresh expiry is checked only when present. Code revocation relies on the model's successful revocation result.
+- **Proposed update:** Describe parameter locations per endpoint; distinguish required registered redirect data from the optional override hook; describe validation guarantees per credential type and the model's role in durable/atomic revocation. Link to the corrected response and authentication sections rather than making blanket claims.
+- **Acceptance:** Every stated library guarantee is tied to an actual handler check. Standards recommendations are clearly separate from implemented behavior; any new standards claims require a separate source verification.
+
+## Priority 3: navigation, incomplete coverage, and contributor instructions
+
+### 15. Repair internal navigation and legacy link syntax
+
+- **Documentation:** The client credentials section of `docs/guide/grant-types.md` links to refresh-token model requirements. `docs/guide/adapters.md` uses literal reStructuredText-style references such as `Request ` and a nonexistent `/api/oauth2-server` route. The PKCE guide and generated model/server pages contain similar legacy references. `docs/index.md` has an โAPI Docsโ action without a link.
+- **Evidence:** The relevant pages exist at `docs/guide/model.md#client-credentials-grant` and `docs/api/server.md`. Generated API pages already provide explicit anchors such as `Model+getClient` and `OAuth2Server+token`; guide links currently rely on long generated heading slugs that are sensitive to signature changes.
+- **Proposed update:** Correct the client credentials target, replace literal legacy references with Markdown/JSDoc links, give the homepage API action `/api/server`, and standardize cross-page API links on verified anchors. Also correct the extension-grant reference whose label says section 4.5 but URL points to 4.4.
+- **Acceptance:** Check rendered page fragments as well as file existence after regeneration. This review did not classify every long heading-slug link as broken; verify them using the renderer before changing them.
+
+### 16. Fill the token guide and mark unsupported internals
+
+- **Documentation:** `docs/guide/token-types.md` consists of empty authorization-code, access-token, and refresh-token sections. API navigation presents MAC and token response classes whose pages provide no implementation-status warning.
+- **Evidence:** `TokenHandler.getTokenType` always creates a bearer token; `AuthorizeHandler` registers only the code response. `MacTokenType` and `TokenResponseType` constructors throw `ServerError('Not implemented.')`. `index.js` exports only a subset of the classes shown in the API, and does not export `Model`.
+- **Proposed update:** Explain each credential's actual fields, expiry expectations, response representation, and lifecycle. Describe refresh rotation (default true), the disabled-rotation response without a new refresh token, scope narrowing, and optional refresh expiry. Mark MAC/implicit response implementations as unsupported placeholders and distinguish package exports from internal reference pages, including the `Model.from` example's import context.
+- **Acceptance:** Readers can identify supported token/response types and public entry points without assuming that every generated class is exported or implemented.
+
+### 17. Align documentation tooling instructions with package scripts
+
+- **Documentation:** The contributing guide advertises `npm run docs`; `docs/build-api.js`'s usage comment refers to `scripts/build-docs-cli.js` and defaults of `src`/`docs`.
+- **Evidence:** `package.json` defines `docs:api`, `docs:dev`, `docs:build`, `docs:preview`, and `docs:setup`, but no `docs` script. The actual generator is `docs/build-api.js` with defaults `lib/` and `docs/api/`. The contributing page symlinks to `CONTRIBUTING.md`.
+- **Proposed update:** Replace the obsolete command with the correct script for the stated operation and correct generator usage comments. Keep the distinction between editing guides, editing source JSDoc, generating API pages, and building the site.
+- **Acceptance:** Every documented local command resolves to an existing script/path and its described effect matches the implementation.
+
+## Implementation sequence
+
+1. Correct Priority 1 guide examples and the associated model/server JSDoc contracts together.
+2. Correct remaining JSDoc signatures, types, defaults, and behavior descriptions without altering executable logic.
+3. Regenerate API Markdown, then update guide links against the resulting anchors.
+4. Complete token/reference coverage, contributor instructions, and navigation fixes.
+5. Validate snippets with in-memory stubs, build documentation, inspect links/fragments, and review the final diff for accidental runtime changes. Keep this plan outside public navigation; decide explicitly whether planning files should be excluded from published pages.
+
+## Verification performed during this review
+
+Eight focused, in-memory runtime checks passed:
+
+- HTTP scope strings parse to arrays and HTTP scope arrays are rejected.
+- The default random token is a 64-character lowercase hexadecimal string.
+- A global `requireClientAuthentication: false` leaves checks enabled, whereas a per-grant false disables the corresponding presence check.
+- `Request.is` returns the matching MIME string for a matching form request.
+- An early token-method error leaves response status/body at their defaults.
+- An authorization denial rejects while leaving a prepared 302 error redirect.
+- A falsy custom authenticate-handler result rejects with a server error.
+- A generator model hook is not executed as a generator and fails authentication.
+
+Source inspection confirmed the remaining findings. No full application test suite, documentation generation, or site build was run: this task changes only the plan, and the normal docs build regenerates existing API files and a guide symlink. Future implementation should run the snippet checks and docs build above in a suitable working copy. External links, current standards wording, and external adapter maintenance were not independently audited.
diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js
index 3a5c9e3d..fbc80037 100644
--- a/lib/errors/invalid-argument-error.js
+++ b/lib/errors/invalid-argument-error.js
@@ -21,12 +21,12 @@ const OAuthError = require('./oauth-error');
* a required method, or `request`/`response` not being instances of this
* library's `Request`/`Response`) it is thrown to your code unchanged, before
* any response is produced โ so you can catch it while wiring up the server.
- * - **While handling a request** (for example, when a `model` returns malformed
- * token data) the `token` and `authorize` handlers normalise it to a
- * `ServerError` (`server_error`) before the response reaches the client,
- * preserving the original error as `.inner`. So when you `catch` errors from
- * `OAuth2Server#token` or `OAuth2Server#authorize`, expect a `ServerError`
- * (inspect its `.inner`) for these cases โ not an `InvalidArgumentError`.
+ * - **Inside the token/authorize normalization blocks**, programming and non-OAuth
+ * errors become `ServerError` with the original error in `.inner`.
+ * - **Before those blocks**, errors propagate unchanged. In particular, authorization
+ * client and user lookup run before its catch, as do token method/content-type
+ * checks. Authentication preserves OAuth errors, including `InvalidArgumentError`.
+ * Integrations must handle early failures locally and avoid exposing internal errors.
*
* @class
*/
diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js
index 19a4a88b..8bdb80de 100644
--- a/lib/grant-types/abstract-grant-type.js
+++ b/lib/grant-types/abstract-grant-type.js
@@ -47,9 +47,9 @@ class AbstractGrantType {
* this implementation will be used.
* Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`.
*
- * @param client
- * @param user
- * @param scope
+ * @param client {ClientData}
+ * @param user {object}
+ * @param scope {string[]|undefined}
* @return {Promise}
*/
async generateAccessToken(client, user, scope) {
@@ -64,6 +64,9 @@ class AbstractGrantType {
/**
* Generate refresh token.
+ * @param client {ClientData}
+ * @param user {object}
+ * @param scope {string[]|undefined}
*/
async generateRefreshToken(client, user, scope) {
if (this.model.generateRefreshToken) {
@@ -93,7 +96,7 @@ class AbstractGrantType {
/**
* Get scope from the request body.
* @param request {Request}
- * @returns {string|undefined}
+ * @returns {string[]|undefined}
*/
getScope(request) {
return parseScope(request.body.scope);
@@ -106,8 +109,8 @@ class AbstractGrantType {
* Otherwise, treats given scope as valid.
* @param user {object}
* @param client {ClientData}
- * @param scope {string}
- * @return {string} the validated scope
+ * @param scope {string[]|undefined}
+ * @return {Promise} the validated scope
* @throws {InvalidScopeError} if the {Model#validateScope} method returned a falsy value
*/
async validateScope(user, client, scope) {
diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js
index 66c3cc14..b92711a4 100644
--- a/lib/grant-types/authorization-code-grant-type.js
+++ b/lib/grant-types/authorization-code-grant-type.js
@@ -78,7 +78,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType {
* Get the authorization code.
* @param request {Request}
* @param client {ClientData}
- * @return {Promise<{user}>}
+ * @return {Promise}
*/
async getAuthorizationCode(request, client) {
@@ -250,7 +250,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType {
* @param user {object}
* @param client {ClientData}
* @param authorizationCode {string}
- * @param requestedScope {string}
+ * @param requestedScope {string[]|undefined}
*
*/
diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js
index c2874f0b..6429634d 100644
--- a/lib/grant-types/client-credentials-grant-type.js
+++ b/lib/grant-types/client-credentials-grant-type.js
@@ -33,6 +33,8 @@ class ClientCredentialsGrantType extends AbstractGrantType {
* Handle client credentials grant.
*
* @see https://tools.ietf.org/html/rfc6749#section-4.4.2
+ * @param request {Request}
+ * @param client {ClientData}
*/
async handle(request, client) {
@@ -52,6 +54,7 @@ class ClientCredentialsGrantType extends AbstractGrantType {
/**
* Retrieve the user using client credentials.
+ * @param client {ClientData}
*/
async getUserFromClient(client) {
@@ -66,6 +69,9 @@ class ClientCredentialsGrantType extends AbstractGrantType {
/**
* Save token.
+ * @param user {object}
+ * @param client {ClientData}
+ * @param requestedScope {string[]|undefined}
*/
async saveToken(user, client, requestedScope) {
diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js
index 2c0128a7..f17b493c 100644
--- a/lib/grant-types/password-grant-type.js
+++ b/lib/grant-types/password-grant-type.js
@@ -36,6 +36,8 @@ class PasswordGrantType extends AbstractGrantType {
* Retrieve the user from the model using a username/password combination.
*
* @see https://tools.ietf.org/html/rfc6749#section-4.3.2
+ * @param request {Request}
+ * @param client {ClientData}
*/
async handle(request, client) {
@@ -55,6 +57,8 @@ class PasswordGrantType extends AbstractGrantType {
/**
* Get user using a username/password combination.
+ * @param request {Request}
+ * @param client {ClientData}
*/
async getUser(request, client) {
@@ -85,6 +89,9 @@ class PasswordGrantType extends AbstractGrantType {
/**
* Save token.
+ * @param user {object}
+ * @param client {ClientData}
+ * @param requestedScope {string[]|undefined}
*/
async saveToken(user, client, requestedScope) {
diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js
index 596a7070..ff039a05 100644
--- a/lib/grant-types/refresh-token-grant-type.js
+++ b/lib/grant-types/refresh-token-grant-type.js
@@ -41,6 +41,8 @@ class RefreshTokenGrantType extends AbstractGrantType {
* Handle refresh token grant.
*
* @see https://tools.ietf.org/html/rfc6749#section-6
+ * @param request {Request}
+ * @param client {ClientData}
*/
async handle(request, client) {
@@ -65,6 +67,8 @@ class RefreshTokenGrantType extends AbstractGrantType {
/**
* Get refresh token.
+ * @param request {Request}
+ * @param client {ClientData}
*/
async getRefreshToken(request, client) {
@@ -109,6 +113,7 @@ class RefreshTokenGrantType extends AbstractGrantType {
* Revoke the refresh token.
*
* @see https://tools.ietf.org/html/rfc6749#section-6
+ * @param token {object}
*/
async revokeToken(token) {
@@ -127,6 +132,9 @@ class RefreshTokenGrantType extends AbstractGrantType {
/**
* Save token.
+ * @param user {object}
+ * @param client {ClientData}
+ * @param scope {string[]|undefined}
*/
async saveToken(user, client, scope) {
diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js
index 4072f385..9543cce2 100644
--- a/lib/handlers/authenticate-handler.js
+++ b/lib/handlers/authenticate-handler.js
@@ -24,7 +24,7 @@ class AuthenticateHandler {
* @constructor
* @param options {object} Server options.
* @param options.model {Model} The Model; this is always required.
- * @param options.scope {string[]|undefined} The scope(s) to authenticate.
+ * @param options.scope {string|string[]|undefined} The scope(s) to authenticate.
* @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects.
* @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects.
* @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request.
@@ -117,7 +117,7 @@ class AuthenticateHandler {
*
* "Clients MUST NOT use more than one method to transmit the token in each request."
*
- * @param {Request}
+ * @param request {Request}
* @see {https://tools.ietf.org/html/rfc6750#section-2}
*/
getTokenFromRequest(request) {
@@ -227,6 +227,7 @@ class AuthenticateHandler {
/**
* Validate access token.
+ * @param accessToken {AccessTokenData}
*/
validateAccessToken(accessToken) {
@@ -243,6 +244,7 @@ class AuthenticateHandler {
/**
* Verify scope.
+ * @param accessToken {AccessTokenData}
*/
async verifyScope(accessToken) {
@@ -255,6 +257,8 @@ class AuthenticateHandler {
/**
* Update response.
+ * @param response {Response}
+ * @param accessToken {AccessTokenData}
*/
updateResponse(response, accessToken) {
diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js
index 4aef3492..efe1705c 100644
--- a/lib/handlers/authorize-handler.js
+++ b/lib/handlers/authorize-handler.js
@@ -69,6 +69,8 @@ class AuthorizeHandler {
/**
* Authorize Handler.
+ * @param request {Request}
+ * @param response {Response}
*/
async handle(request, response) {
@@ -144,6 +146,9 @@ class AuthorizeHandler {
/**
* Generate authorization code.
+ * @param client {ClientData}
+ * @param user {object}
+ * @param scope {string[]|undefined}
*/
async generateAuthorizationCode(client, user, scope) {
@@ -166,6 +171,7 @@ class AuthorizeHandler {
/**
* Get the client from the model.
+ * @param request {Request}
*/
async getClient(request) {
@@ -217,6 +223,10 @@ class AuthorizeHandler {
/**
* Validate requested scope.
+ * @param user {object}
+ * @param client {ClientData}
+ * @param scope {string[]|undefined}
+ * @return {Promise} The accepted scopes.
*/
async validateScope(user, client, scope) {
if (this.model.validateScope) {
@@ -234,6 +244,8 @@ class AuthorizeHandler {
/**
* Get scope from the request.
+ * @param request {Request}
+ * @return {string[]|undefined}
*/
getScope(request) {
@@ -244,6 +256,7 @@ class AuthorizeHandler {
/**
* Get state from the request.
+ * @param request {Request}
*/
getState(request) {
@@ -261,6 +274,8 @@ class AuthorizeHandler {
/**
* Get user by calling the authenticate middleware.
+ * @param request {Request}
+ * @param response {Response}
*/
async getUser(request, response) {
@@ -280,6 +295,8 @@ class AuthorizeHandler {
/**
* Get redirect URI.
+ * @param request {Request}
+ * @param client {ClientData}
*/
getRedirectUri(request, client) {
@@ -288,6 +305,14 @@ class AuthorizeHandler {
/**
* Save authorization code.
+ * @param authorizationCode {string}
+ * @param expiresAt {Date}
+ * @param scope {string[]|undefined}
+ * @param client {ClientData}
+ * @param redirectUri {string}
+ * @param user {object}
+ * @param codeChallenge {string|undefined}
+ * @param codeChallengeMethod {string|undefined}
*/
async saveAuthorizationCode(
@@ -329,6 +354,7 @@ class AuthorizeHandler {
}
/**
* Get response type.
+ * @param request {Request}
*/
getResponseType(request) {
@@ -347,6 +373,8 @@ class AuthorizeHandler {
/**
* Build a successful response that redirects the user-agent to the client-provided url.
+ * @param redirectUri {string}
+ * @param responseType {object}
*/
buildSuccessRedirectUri(redirectUri, responseType) {
@@ -355,6 +383,8 @@ class AuthorizeHandler {
/**
* Build an error response that redirects the user-agent to the client-provided url.
+ * @param redirectUri {string}
+ * @param error {Error}
*/
buildErrorRedirectUri(redirectUri, error) {
@@ -373,6 +403,9 @@ class AuthorizeHandler {
/**
* Update response with the redirect uri and the state parameter, if available.
+ * @param response {Response}
+ * @param redirectUri {object} Parsed URL returned by the redirect builders.
+ * @param state {string|undefined}
*/
updateResponse(response, redirectUri, state) {
@@ -400,6 +433,7 @@ class AuthorizeHandler {
* @see https://www.rfc-editor.org/rfc/rfc7636#section-4.3
* @throws {InvalidRequestError} if request contains unsupported code_challenge_method
* (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4)
+ * @param request {Request}
*/
getCodeChallengeMethod(request) {
const algorithm = request.body.code_challenge_method || request.query.code_challenge_method;
diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js
index 895ef9c8..7a818d3e 100644
--- a/lib/handlers/token-handler.js
+++ b/lib/handlers/token-handler.js
@@ -67,6 +67,8 @@ class TokenHandler {
/**
* Token Handler.
+ * @param request {Request}
+ * @param response {Response}
*/
async handle(request, response) {
@@ -115,6 +117,8 @@ class TokenHandler {
/**
* Get the client from the model.
+ * @param request {Request}
+ * @param response {Response}
*/
async getClient(request, response) {
@@ -176,6 +180,7 @@ class TokenHandler {
* the `client_id` and `client_secret` can be embedded in the body.
*
* @see https://tools.ietf.org/html/rfc6749#section-2.3.1
+ * @param request {Request}
*/
getClientCredentials(request) {
@@ -211,6 +216,8 @@ class TokenHandler {
/**
* Handle grant type.
+ * @param request {Request}
+ * @param client {ClientData}
*/
async handleGrantType(request, client) {
@@ -250,6 +257,7 @@ class TokenHandler {
/**
* Get access token lifetime.
+ * @param client {ClientData}
*/
getAccessTokenLifetime(client) {
@@ -258,6 +266,7 @@ class TokenHandler {
/**
* Get refresh token lifetime.
+ * @param client {ClientData}
*/
getRefreshTokenLifetime(client) {
@@ -266,6 +275,7 @@ class TokenHandler {
/**
* Get token type.
+ * @param model {TokenModel}
*/
getTokenType(model) {
@@ -280,6 +290,8 @@ class TokenHandler {
/**
* Update response when a token is generated.
+ * @param response {Response}
+ * @param tokenType {BearerTokenType}
*/
updateSuccessResponse(response, tokenType) {
@@ -297,6 +309,8 @@ class TokenHandler {
/**
* Update response when an error is thrown.
+ * @param response {Response}
+ * @param error {Error}
*/
updateErrorResponse(response, error) {
@@ -310,6 +324,7 @@ class TokenHandler {
/**
* Given a grant type, check if client authentication is required
+ * @param grantType {string}
*/
isClientAuthenticationRequired(grantType) {
if (Object.keys(this.requireClientAuthentication).length > 0) {
diff --git a/lib/model.js b/lib/model.js
index cbf4e843..2e1e7723 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -10,7 +10,7 @@ const ServerError = require('./errors/server-error');
* @description An `Object` representing the access token and associated data. `token.client` and `token.user` can carry additional properties that will be ignored by *oauth2-server*.
* @property accessToken {string} The access token passed to `getAccessToken()`
* @property accessTokenExpiresAt {Date} The expiry time of the access token.
- * @property scope {string[]} The authorized scope of the access token.
+ * @property [scope] {string[]} The authorized scope of the access token.
* @property client {object} The client associated with the access token.
* @property client.id {string} A unique string identifying the client.
* @property user {object} The user associated with the access token.
@@ -20,8 +20,8 @@ const ServerError = require('./errors/server-error');
* @typedef RefreshTokenData
* @description An `Object` representing the refresh token and associated data. `token.client` and `token.user` can carry additional properties that will be ignored by *oauth2-server*.
* @property refreshToken {string} The refresh token passed to `getRefreshToken()`
- * @property refreshTokenExpiresAt {Date} The expiry time of the refresh token.
- * @property scope {string[]} The authorized scope of the refresh token.
+ * @property [refreshTokenExpiresAt] {Date} The expiry time of the refresh token.
+ * @property [scope] {string[]} The authorized scope of the refresh token.
* @property client {ClientData} The client associated with the refresh token.
* @property user {object} The user associated with the access token.
*/
@@ -29,10 +29,12 @@ const ServerError = require('./errors/server-error');
/**
* @typedef AuthorizationCodeData
* @description An `Object` representing the authorization code and associated data. `code.client` and `code.user` can carry additional properties that will be ignored by *oauth2-server*.
+ * @property [codeChallenge] {string} Stored PKCE challenge; preserve during save/load.
+ * @property [codeChallengeMethod] {string} Stored PKCE method (S256 or plain).
* @property authorizationCode {string} The authorization code passed to `getAuthorizationCode()`.
* @property expiresAt {Date} The expiry time of the authorization code.
* @property redirectUri {string} The redirect URI of the authorization code.
- * @property scope {string[]} The authorized scope of the authorization code.
+ * @property [scope] {string[]} The authorized scope of the authorization code.
* @property client {ClientData} The client associated with the authorization code.
* @property user {object} The user associated with the access token.
*/
@@ -44,8 +46,8 @@ const ServerError = require('./errors/server-error');
* @property id {string} A unique string identifying the client.
* @property redirectUris {string[]} Redirect URIs allowed for the client. Required for the `authorization_code` grant.
* @property grants {string[]} Grant types allowed for the client.
- * @property accessTokenLifetime {number} Client-specific lifetime of generated access tokens in seconds.
- * @property refreshTokenLifetime {number} Client-specific lifetime of generated refresh tokens in seconds.
+ * @property [accessTokenLifetime] {number} Client-specific lifetime of generated access tokens in seconds.
+ * @property [refreshTokenLifetime] {number} Client-specific lifetime of generated refresh tokens in seconds.
*/
/**
@@ -56,11 +58,16 @@ const ServerError = require('./errors/server-error');
*
* Each model function is resolved async by default.
* This implies that async and sync model functions,
- * as well as generators, are supported.
+ * returning values or native Promises are supported. Direct generator hooks are not run.
+ * Promise return types below also permit the corresponding synchronous value.
+ * This class is an internal reference, not exported by the package entry point;
+ * applications normally pass a plain object of hooks.
*
* @example
+ * // Repository-internal import, not a public package export:
+ * const Model = require('./lib/model');
* const model = Model.from({
- * getClient: () => { ... }
+ * getClient: async (id) => db.queryClient({ id })
* })
*/
class Model {
@@ -97,12 +104,13 @@ class Model {
* - `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.
+ * Authorization always calls `getClient(clientId, null)`, including for confidential
+ * clients. Token requests pass the supplied secret; PKCE or a per-grant option
+ * can permit its absence. Always validate supplied secrets. An absent secret
+ * alone cannot identify the endpoint or distinguish public/confidential clients.
+ * Confidential token requests still require authentication: use endpoint-specific
+ * model wrappers as shown in the [PKCE guide](../guide/pkce.md#pkce-client-authentication-and-refresh-tokens).
+ * Universally rejecting missing secrets here breaks confidential-client authorization.
*
* @async
* @param clientId {string} The client id of the client to retrieve.
@@ -125,51 +133,40 @@ class Model {
* - `refresh_token` grant
* - `password` grant
*
- * If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client.
+ * If the `allowExtendedTokenAttributes` server option is enabled (see [OAuth2Server#token](server.md#OAuth2Server+token)) any additional attributes set on the result are copied to the token response sent to the client.
*
* @async
* @instance
* @param token {object} The token(s) to be saved.
* @param token.accessToken {string} The access token to be saved.
* @param token.accessTokenExpiresAt {Date} The expiry time of the access token.
- * @param token.refreshToken {string} The refresh token to be saved.
- * @param token.refreshTokenExpiresAt {Date} The expiry time of the refresh token.
- * @param token.scope {string[]} The authorized scope of the token(s)
+ * @param [token.refreshToken] {string} The refresh token to be saved.
+ * @param [token.refreshTokenExpiresAt] {Date} The expiry time of the refresh token.
+ * @param [token.scope] {string[]} The authorized scope of the token(s)
* @param client {ClientData} The client associated with the token(s).
* @param user {object} The user associated with the token(s).
* @return {Promise