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). - -[![Tests](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml) -[![CodeQL Semantic Analysis](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml) -[![Documentation Status](https://readthedocs.org/projects/node-oauthoauth2-server/badge/?version=latest)](https://node-oauthoauth2-server.readthedocs.io/en/latest/?badge=latest) [![Project Status: Active โ€“ The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![npm Version](https://img.shields.io/npm/v/@node-oauth/oauth2-server?label=version)](https://www.npmjs.com/package/@node-oauth/oauth2-server) [![npm Downloads/Week](https://img.shields.io/npm/dw/@node-oauth/oauth2-server)](https://www.npmjs.com/package/@node-oauth/oauth2-server) [![GitHub License](https://img.shields.io/github/license/node-oauth/node-oauth2-server)](https://github.com/node-oauth/node-oauth2-server/blob/master/LICENSE) +[![Tests](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml) [![Formatted with Biome](https://img.shields.io/badge/Formatted_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev/) +[![CodeQL Semantic Analysis](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml) +[![Deploy VitePress site to Pages](https://github.com/node-oauth/node-oauth2-server/actions/workflows/deploy_docs.yml/badge.svg)](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