diff --git a/.github/workflows/tests-release.yml b/.github/workflows/tests-release.yml index da006a27..2bd4e253 100644 --- a/.github/workflows/tests-release.yml +++ b/.github/workflows/tests-release.yml @@ -10,7 +10,6 @@ on: branches: - release-* # all release- branches - jobs: # STEP 1 - NPM Audit @@ -23,13 +22,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 20 - # install to create local package-lock.json but don't cache the files - # also: no audit for dev dependencies - - run: npm i --package-lock-only && npm audit --production + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + # install to create local package-lock.json but don't cache the files + # also: no audit for dev dependencies + - run: npm i --package-lock-only && npm audit --production # STEP 2 - basic unit tests @@ -40,34 +39,34 @@ jobs: needs: [audit] strategy: matrix: - node: [14, 16, 18] + node: [16, 18, 20] steps: - - name: Checkout ${{ matrix.node }} - uses: actions/checkout@v3 - - - name: Setup node ${{ matrix.node }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - - - name: Cache dependencies ${{ matrix.node }} - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node }} - # for this workflow we also require npm audit to pass - - run: npm i - - run: npm run test:coverage - - # with the following action we enforce PRs to have a high coverage - # and ensure, changes are tested well enough so that coverage won't fail - - name: check coverage - uses: VeryGoodOpenSource/very_good_coverage@v1.2.0 - with: - path: './coverage/lcov.info' - min_coverage: 95 + - name: Checkout ${{ matrix.node }} + uses: actions/checkout@v3 + + - name: Setup node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Cache dependencies ${{ matrix.node }} + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ matrix.node }} + # for this workflow we also require npm audit to pass + - run: npm i + - run: npm run test:coverage + + # with the following action we enforce PRs to have a high coverage + # and ensure, changes are tested well enough so that coverage won't fail + - name: check coverage + uses: VeryGoodOpenSource/very_good_coverage@v1.2.0 + with: + path: './coverage/lcov.info' + min_coverage: 95 # STEP 3 - Integration tests @@ -80,41 +79,41 @@ jobs: needs: [unittest] strategy: matrix: - node: [14, 16, 18] # TODO get running for node 16+ + node: [16, 18, 20] # TODO get running for node 16+ steps: - # checkout this repo - - name: Checkout ${{ matrix.node }} - uses: actions/checkout@v3 - - # checkout express-adapter repo - - name: Checkout express-adapter ${{ matrix.node }} - uses: actions/checkout@v3 - with: - repository: node-oauth/express-oauth-server - path: github/testing/express - - - name: Setup node ${{ matrix.node }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - - - name: Cache dependencies ${{ matrix.node }} - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node }}-node-oauth/express-oauth-server-${{ hashFiles('github/testing/express/**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node }}-node-oauth/express-oauth-server - - # in order to test the adapter we need to use the current checkout - # and install it as local dependency - # we just cloned and install it as local dependency - # xxx: added bluebird as explicit dependency - - run: | - cd github/testing/express - npm i - npm install ../../../ - npm run test + # checkout this repo + - name: Checkout ${{ matrix.node }} + uses: actions/checkout@v3 + + # checkout express-adapter repo + - name: Checkout express-adapter ${{ matrix.node }} + uses: actions/checkout@v3 + with: + repository: node-oauth/express-oauth-server + path: github/testing/express + + - name: Setup node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Cache dependencies ${{ matrix.node }} + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ matrix.node }}-node-oauth/express-oauth-server-${{ hashFiles('github/testing/express/**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ matrix.node }}-node-oauth/express-oauth-server + + # in order to test the adapter we need to use the current checkout + # and install it as local dependency + # we just cloned and install it as local dependency + # xxx: added bluebird as explicit dependency + - run: | + cd github/testing/express + npm i + npm install https://github.com/node-oauth/node-oauth2-server.git#${{ github.ref_name }} + npm run test # todo repeat with other adapters @@ -139,13 +138,13 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - # we always publish targeting the lowest supported node version - node-version: 16 - registry-url: $registry-url(npm) - - run: npm i - - run: npm publish --dry-run - env: - NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + # we always publish targeting the lowest supported node version + node-version: 16 + registry-url: $registry-url(npm) + - run: npm i + - run: npm publish --dry-run + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..bc2b3c65 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 608cb59c..79ef175d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,21 @@ ## 5.0.0 +This release contains several breaking changes. +Please carefully consult the documentation while updating. + - removed `bluebird` and `promisify-any` - uses native Promises and `async/await` everywhere - drop support for Node 14 (EOL), setting Node 16 as `engine` in `package.json` - this is a breaking change, because **it removes callback support** for `OAuthServer` and your model implementation. +- fixed missing await in calling generateAuthorizationCode in AuthorizeHandler +- fix scope validation bug +- revoke code before validating redirect URI +- improved Bearer token validation +- validate scope as an array of strings (breaking change) +- model support for retrieving user based on client +- more tests added; test coverage improved ## 4.2.0 ### Fixed @@ -51,7 +61,7 @@ - Upgrades all code from ES5 to ES6, where possible. ## 4.1.0 -### Changed +### Changed * Bump dev dependencies to resolve vulnerabilities * Replaced jshint with eslint along with should and chai * Use sha256 when generating tokens diff --git a/README.md b/README.md index 1dc86361..5f01c4af 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ If you're using one of those frameworks it is strongly recommended to use the re ## Features - Supports `authorization_code`, `client_credentials`, `refresh_token` and `password` grant, as well as *extension grants*, with scopes. -- Can be used with *promises*, *Node-style callbacks*, *ES6 generators* and *async*/*await* (using [Babel](https://babeljs.io)). +- 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 @@ -35,7 +35,11 @@ If you're using one of those frameworks it is strongly recommended to use the re ## Documentation -[Documentation](https://node-oauthoauth2-server.readthedocs.io/en/latest/) is hosted on Read the Docs. +Documentation is hosted on Read the Docs. We have multiple versions of the docs available: + +- [stable](https://node-oauthoauth2-server.readthedocs.io/en/master/) (master branch) +- [development](https://node-oauthoauth2-server.readthedocs.io/en/development/) (development branch) + Please leave an issue if something is confusing or missing in the docs. ## Examples diff --git a/docs/api/errors/access-denied-error.rst b/docs/api/errors/access-denied-error.rst index e561c5b0..11e61ec9 100644 --- a/docs/api/errors/access-denied-error.rst +++ b/docs/api/errors/access-denied-error.rst @@ -6,7 +6,7 @@ The resource owner or authorization server denied the request. See :rfc:`Section :: - const AccessDeniedError = require('oauth2-server/lib/errors/access-denied-error'); + const AccessDeniedError = require('@node-oauth/oauth2-server/lib/errors/access-denied-error'); -------- diff --git a/docs/api/errors/insufficient-scope-error.rst b/docs/api/errors/insufficient-scope-error.rst index be3539de..b19e2acb 100644 --- a/docs/api/errors/insufficient-scope-error.rst +++ b/docs/api/errors/insufficient-scope-error.rst @@ -6,7 +6,7 @@ The request requires higher privileges than provided by the access token. See :r :: - const InsufficientScopeError = require('oauth2-server/lib/errors/insufficient-scope-error'); + const InsufficientScopeError = require('@node-oauth/oauth2-server/lib/errors/insufficient-scope-error'); -------- diff --git a/docs/api/errors/invalid-argument-error.rst b/docs/api/errors/invalid-argument-error.rst index 650e1d9f..11b554eb 100644 --- a/docs/api/errors/invalid-argument-error.rst +++ b/docs/api/errors/invalid-argument-error.rst @@ -6,7 +6,7 @@ An invalid argument was encountered. :: - const InvalidArgumentError = require('oauth2-server/lib/errors/invalid-argument-error'); + const InvalidArgumentError = require('@node-oauth/oauth2-server/lib/errors/invalid-argument-error'); .. note:: This error indicates that the module is used incorrectly (i.e., there is a programming error) and should never be seen because of external errors (like invalid data sent by a client). diff --git a/docs/api/errors/invalid-client-error.rst b/docs/api/errors/invalid-client-error.rst index d25a4934..5ddd0a40 100644 --- a/docs/api/errors/invalid-client-error.rst +++ b/docs/api/errors/invalid-client-error.rst @@ -6,7 +6,7 @@ Client authentication failed (e.g., unknown client, no client authentication inc :: - const InvalidClientError = require('oauth2-server/lib/errors/invalid-client-error'); + const InvalidClientError = require('@node-oauth/oauth2-server/lib/errors/invalid-client-error'); -------- diff --git a/docs/api/errors/invalid-grant-error.rst b/docs/api/errors/invalid-grant-error.rst index 8f2a9ba2..79317149 100644 --- a/docs/api/errors/invalid-grant-error.rst +++ b/docs/api/errors/invalid-grant-error.rst @@ -6,7 +6,7 @@ The provided authorization grant (e.g., authorization code, resource owner crede :: - const InvalidGrantError = require('oauth2-server/lib/errors/invalid-grant-error'); + const InvalidGrantError = require('@node-oauth/oauth2-server/lib/errors/invalid-grant-error'); -------- diff --git a/docs/api/errors/invalid-request-error.rst b/docs/api/errors/invalid-request-error.rst index 119ab40e..bbb38c44 100644 --- a/docs/api/errors/invalid-request-error.rst +++ b/docs/api/errors/invalid-request-error.rst @@ -6,7 +6,7 @@ The request is missing a required parameter, includes an invalid parameter value :: - const InvalidRequestError = require('oauth2-server/lib/errors/invalid-request-error'); + const InvalidRequestError = require('@node-oauth/oauth2-server/lib/errors/invalid-request-error'); -------- diff --git a/docs/api/errors/invalid-scope-error.rst b/docs/api/errors/invalid-scope-error.rst index 801930f9..01c70d26 100644 --- a/docs/api/errors/invalid-scope-error.rst +++ b/docs/api/errors/invalid-scope-error.rst @@ -6,7 +6,7 @@ The requested scope is invalid, unknown, or malformed. See :rfc:`Section 4.1.2.1 :: - const InvalidScopeError = require('oauth2-server/lib/errors/invalid-scope-error'); + const InvalidScopeError = require('@node-oauth/oauth2-server/lib/errors/invalid-scope-error'); -------- diff --git a/docs/api/errors/invalid-token-error.rst b/docs/api/errors/invalid-token-error.rst index 21ffad8f..fc0da035 100644 --- a/docs/api/errors/invalid-token-error.rst +++ b/docs/api/errors/invalid-token-error.rst @@ -6,7 +6,7 @@ The access token provided is expired, revoked, malformed, or invalid for other r :: - const InvalidTokenError = require('oauth2-server/lib/errors/invalid-token-error'); + const InvalidTokenError = require('@node-oauth/oauth2-server/lib/errors/invalid-token-error'); -------- diff --git a/docs/api/errors/oauth-error.rst b/docs/api/errors/oauth-error.rst index c7f1d861..83be4659 100644 --- a/docs/api/errors/oauth-error.rst +++ b/docs/api/errors/oauth-error.rst @@ -6,7 +6,7 @@ Base class for all errors returned by this module. :: - const OAuthError = require('oauth2-server/lib/errors/oauth-error'); + const OAuthError = require('@node-oauth/oauth2-server/lib/errors/oauth-error'); -------- diff --git a/docs/api/errors/server-error.rst b/docs/api/errors/server-error.rst index 13f436ed..7a2dcf90 100644 --- a/docs/api/errors/server-error.rst +++ b/docs/api/errors/server-error.rst @@ -6,7 +6,7 @@ The authorization server encountered an unexpected condition that prevented it f :: - const ServerError = require('oauth2-server/lib/errors/server-error'); + const ServerError = require('@node-oauth/oauth2-server/lib/errors/server-error'); ``ServerError`` is used to wrap unknown exceptions encountered during request processing. diff --git a/docs/api/errors/unauthorized-client-error.rst b/docs/api/errors/unauthorized-client-error.rst index d04cb080..9d104cac 100644 --- a/docs/api/errors/unauthorized-client-error.rst +++ b/docs/api/errors/unauthorized-client-error.rst @@ -6,7 +6,7 @@ The authenticated client is not authorized to use this authorization grant type. :: - const UnauthorizedClientError = require('oauth2-server/lib/errors/unauthorized-client-error'); + const UnauthorizedClientError = require('@node-oauth/oauth2-server/lib/errors/unauthorized-client-error'); -------- diff --git a/docs/api/errors/unauthorized-request-error.rst b/docs/api/errors/unauthorized-request-error.rst index 495f5f8c..9ed24675 100644 --- a/docs/api/errors/unauthorized-request-error.rst +++ b/docs/api/errors/unauthorized-request-error.rst @@ -6,7 +6,7 @@ The request lacked any authentication information or the client attempted to use :: - const UnauthorizedRequestError = require('oauth2-server/lib/errors/unauthorized-request-error'); + const UnauthorizedRequestError = require('@node-oauth/oauth2-server/lib/errors/unauthorized-request-error'); According to :rfc:`Section 3.1 of RFC 6750 <6750#section-3.1>` you should just fail the request with ``401 Unauthorized`` and not send any error information in the body if this error occurs: diff --git a/docs/api/errors/unsupported-grant-type-error.rst b/docs/api/errors/unsupported-grant-type-error.rst index d2fe49f7..1e812ed7 100644 --- a/docs/api/errors/unsupported-grant-type-error.rst +++ b/docs/api/errors/unsupported-grant-type-error.rst @@ -6,7 +6,7 @@ The authorization grant type is not supported by the authorization server. See : :: - const UnsupportedGrantTypeError = require('oauth2-server/lib/errors/unsupported-grant-type-error'); + const UnsupportedGrantTypeError = require('@node-oauth/oauth2-server/lib/errors/unsupported-grant-type-error'); -------- diff --git a/docs/api/errors/unsupported-response-type-error.rst b/docs/api/errors/unsupported-response-type-error.rst index 28974eba..c9ee0fd3 100644 --- a/docs/api/errors/unsupported-response-type-error.rst +++ b/docs/api/errors/unsupported-response-type-error.rst @@ -6,7 +6,7 @@ The authorization server does not supported obtaining an authorization code usin :: - const UnsupportedResponseTypeError = require('oauth2-server/lib/errors/unsupported-response-type-error'); + const UnsupportedResponseTypeError = require('@node-oauth/oauth2-server/lib/errors/unsupported-response-type-error'); -------- diff --git a/docs/api/oauth2-server.rst b/docs/api/oauth2-server.rst index 48acf538..2cb6cda4 100644 --- a/docs/api/oauth2-server.rst +++ b/docs/api/oauth2-server.rst @@ -6,7 +6,7 @@ Represents an OAuth2 server instance. :: - const OAuth2Server = require('oauth2-server'); + const OAuth2Server = require('@node-oauth/oauth2-server'); -------- @@ -57,7 +57,7 @@ Advanced example with additional options: .. _OAuth2Server#authenticate: -``authenticate(request, response, [options], [callback])`` +``authenticate(request, response, [options])`` ========================================================== Authenticates a request. @@ -73,7 +73,7 @@ Authenticates a request. +------------------------------------------------+-----------------+-----------------------------------------------------------------------+ | [options={}] | Object | Handler options. | +------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [options.scope=undefined] | String | The scope(s) to authenticate. | +| [options.scope=undefined] | String[] | The scope(s) to authenticate. | +------------------------------------------------+-----------------+-----------------------------------------------------------------------+ | [options.addAcceptedScopesHeader=true] | Boolean | Set the ``X-Accepted-OAuth-Scopes`` HTTP header on response objects. | +------------------------------------------------+-----------------+-----------------------------------------------------------------------+ @@ -81,8 +81,6 @@ Authenticates a request. +------------------------------------------------+-----------------+-----------------------------------------------------------------------+ | [options.allowBearerTokensInQueryString=false] | Boolean | Allow clients to pass bearer tokens in the query string of a request. | +------------------------------------------------+-----------------+-----------------------------------------------------------------------+ -| [callback=undefined] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+------------------------------------------------+-----------------+-----------------------------------------------------------------------+ **Return value:** @@ -94,8 +92,6 @@ Possible errors include but are not limited to: :doc:`/api/errors/unauthorized-request-error`: The protected resource request failed authentication. -The returned ``Promise`` **must** be ignored if ``callback`` is used. - **Remarks:** :: @@ -121,7 +117,7 @@ The returned ``Promise`` **must** be ignored if ``callback`` is used. .. _OAuth2Server#authorize: -``authorize(request, response, [options], [callback])`` +``authorize(request, response, [options])`` ======================================================= Authorizes a token request. @@ -145,8 +141,6 @@ Authorizes a token request. +-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ | [options.authorizationCodeLifetime=300] | Number | Lifetime of generated authorization codes in seconds (default = 5 minutes). | +-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [callback=undefined] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ **Return value:** @@ -158,8 +152,6 @@ Possible errors include but are not limited to: :doc:`/api/errors/access-denied-error` The resource owner denied the access request (i.e. ``request.query.allow`` was ``'false'``). -The returned ``Promise`` **must** be ignored if ``callback`` is used. - **Remarks:** If ``request.query.allowed`` equals the string ``'false'`` the access request is denied and the returned promise is rejected with an :doc:`/api/errors/access-denied-error`. @@ -211,7 +203,7 @@ When working with a session-based login mechanism, the handler can simply look l .. _OAuth2Server#token: -``token(request, response, [options], [callback])`` +``token(request, response, [options])`` =================================================== Retrieves a new token for an authorized token request. @@ -239,8 +231,6 @@ Retrieves a new token for an authorized token request. +----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ | [options.extendedGrantTypes={}] | Object | Additional supported grant types. | +----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ -| [callback=undefined] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+----------------------------------------------+-----------------+-------------------------------------------------------------------------------------------+ **Return value:** @@ -252,8 +242,6 @@ Possible errors include but are not limited to: :doc:`/api/errors/invalid-grant-error`: The access token request was invalid or not authorized. -The returned ``Promise`` **must** be ignored if ``callback`` is used. - **Remarks:** If ``options.allowExtendedTokenAttributes`` is ``true`` any additional properties set on the object returned from :ref:`Model#saveToken() ` are copied to the token response sent to the client. diff --git a/docs/api/request.rst b/docs/api/request.rst index b8f8963a..7d5f4cad 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -6,7 +6,7 @@ Represents an incoming HTTP request. :: - const Request = require('oauth2-server').Request; + const Request = require('@node-oauth/oauth2-server').Request; -------- @@ -50,7 +50,7 @@ To convert `Express' request`_ to a ``Request`` simply pass ``req`` as ``options :: function(req, res, next) { - var request = new Request(req); + let request = new Request(req); // ... } diff --git a/docs/api/response.rst b/docs/api/response.rst index 48cc36dc..2c5d3326 100644 --- a/docs/api/response.rst +++ b/docs/api/response.rst @@ -6,7 +6,7 @@ Represents an outgoing HTTP response. :: - const Response = require('oauth2-server').Response; + const Response = require('@node-oauth/oauth2-server').Response; -------- @@ -46,7 +46,7 @@ To convert `Express' response`_ to a ``Response`` simply pass ``res`` as ``optio :: function(req, res, next) { - var response = new Response(res); + let response = new Response(res); // ... } diff --git a/docs/conf.py b/docs/conf.py index d9aae790..2abf9c3f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# oauth2-server documentation build configuration file, created by +# @node-oauth/oauth2-server documentation build configuration file, created by # sphinx-quickstart on Thu Nov 17 16:47:05 2016. # # This file is execfile()d with the current directory set to its containing dir. @@ -28,7 +28,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'sphinx.ext.todo'] +extensions = ['sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'sphinx.ext.todo', 'sphinx_rtd_theme'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -94,7 +94,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -272,5 +272,5 @@ highlight_language = 'js' def setup(app): - app.add_stylesheet('custom.css') + app.add_css_file('custom.css') diff --git a/docs/docs/adapters.rst b/docs/docs/adapters.rst index c302d34e..139995ee 100644 --- a/docs/docs/adapters.rst +++ b/docs/docs/adapters.rst @@ -2,14 +2,14 @@ Adapters ========== -The *oauth2-server* module is typically not used directly but through one of the available adapters, converting the interface to a suitable one for the HTTP server framework in use. +The *@node-oauth/oauth2-server* module is typically not used directly but through one of the available adapters, converting the interface to a suitable one for the HTTP server framework in use. -.. framework-agnostic but there are several officially supported adapters available for popular HTTP server frameworks such as Express_ and Koa_. +.. framework-agnostic but there are several officially supported adapters available for popular HTTP server frameworks such as Express_ and Koa_ (not maintained by us). - express-oauth-server_ for Express_ - koa-oauth-server_ for Koa_ -.. _express-oauth-server: https://npmjs.org/package/express-oauth-server +.. _express-oauth-server: https://www.npmjs.com/package/@node-oauth/express-oauth-server .. _Express: https://npmjs.org/package/express .. _koa-oauth-server: https://npmjs.org/package/koa-oauth-server .. _Koa: https://npmjs.org/package/koa @@ -32,5 +32,5 @@ Adapters typically do the following: - Copy all fields from the :doc:`Response ` back to the framework-specific request object and send it. -Adapters should preserve functionality provided by *oauth2-server* but are free to add additional features that make sense for the respective HTTP server framework. +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. diff --git a/docs/docs/getting-started.rst b/docs/docs/getting-started.rst index ff2c1156..9d86c15b 100644 --- a/docs/docs/getting-started.rst +++ b/docs/docs/getting-started.rst @@ -9,16 +9,16 @@ Installation oauth2-server_ is available via npm_. -.. _oauth2-server: https://npmjs.org/package/oauth2-server +.. _oauth2-server: https://www.npmjs.com/package/@node-oauth/oauth2-server .. _npm: https://npmjs.org .. code-block:: sh - $ npm install oauth2-server + $ npm install @node-oauth/oauth2-server -.. note:: The *oauth2-server* module is framework-agnostic but there are several officially supported adapters available for popular HTTP server frameworks such as Express_ and Koa_. If you're using one of those frameworks it is strongly recommended to use the respective adapter module instead of rolling your own. +.. note:: The *@node-oauth/oauth2-server* module is framework-agnostic but there are several officially supported adapters available for popular HTTP server frameworks such as Express_ and Koa_. If you're using one of those frameworks it is strongly recommended to use the respective adapter module instead of rolling your own. -.. _Express: https://npmjs.org/package/express-oauth-server +.. _Express: https://www.npmjs.com/package/@node-oauth/express-oauth-server .. _Koa: https://npmjs.org/package/koa-oauth-server @@ -28,13 +28,12 @@ Features ======== - Supports :ref:`authorization code `, :ref:`client credentials `, :ref:`refresh token ` and :ref:`password ` grant, as well as :ref:`extension grants `, with scopes. -- Can be used with *promises*, *Node-style callbacks*, *ES6 generators* and *async*/*await* (using Babel_). +- Can be used with *promises*, *ES6 generators* and *async*/*await*. - Fully :rfc:`6749` and :rfc:`6750` compliant. - Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc. - Complete `test suite`_. -.. _Babel: https://babeljs.io -.. _test suite: https://github.com/oauthjs/node-oauth2-server/tree/master/test +.. _test suite: https://github.com/node-oauth/node-oauth2-server/tree/master/test .. _quick-start: @@ -46,7 +45,7 @@ Quick Start :: - const OAuth2Server = require('oauth2-server'); + const OAuth2Server = require('@node-oauth/oauth2-server'); const oauth = new OAuth2Server({ model: require('./model') @@ -78,7 +77,7 @@ Quick Start :: - const AccessDeniedError = require('oauth2-server/lib/errors/access-denied-error'); + const AccessDeniedError = require('@node-oauth/oauth2-server/lib/errors/access-denied-error'); oauth.authorize(request, response) .then((code) => { diff --git a/docs/index.rst b/docs/index.rst index 4a7c3415..7c1ea417 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,13 @@ -=============== - oauth2-server -=============== +========================== + @node-oauth/oauth2-server +========================== -oauth2-server_ is a complete, compliant and well tested module for implementing an OAuth2 server in Node.js_. The project is `hosted on GitHub`_ and the included test suite is automatically `run on Travis CI`_. +oauth2-server_ is a complete, compliant and well tested module for implementing an OAuth2 server in Node.js_. The project is `hosted on GitHub`_ and the included test suite is automatically `run on GitHub CI`_. -.. _oauth2-server: https://npmjs.org/package/oauth2-server +.. _oauth2-server: https://www.npmjs.com/package/@node-oauth/oauth2-server .. _Node.js: https://nodejs.org -.. _hosted on GitHub: https://github.com/oauthjs/node-oauth2-server -.. _run on Travis CI: https://travis-ci.org/oauthjs/node-oauth2-server +.. _hosted on GitHub: https://github.com/node-oauth/node-oauth2-server +.. _run on GitHub CI: https://github.com/node-oauth/node-oauth2-server/actions :ref:`installation` @@ -17,7 +17,7 @@ Example Usage :: - const OAuth2Server = require('oauth2-server'); + const OAuth2Server = require('@node-oauth/oauth2-server'); const Request = OAuth2Server.Request; const Response = OAuth2Server.Response; @@ -45,25 +45,23 @@ Example Usage See the :doc:`/model/spec` of what is required from the model passed to :doc:`/api/oauth2-server`. +Contents +-------- .. toctree:: - :hidden: - Home .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: User Documentation - :hidden: docs/getting-started docs/adapters .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: API :includehidden: - :hidden: api/oauth2-server api/request @@ -71,18 +69,18 @@ See the :doc:`/model/spec` of what is required from the model passed to :doc:`/a api/errors/index .. toctree:: - :maxdepth: 3 + :maxdepth: 1 :caption: Model - :hidden: model/overview model/spec .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Miscellaneous - :hidden: misc/extension-grants + misc/pkce + misc/migrating-to-v5 misc/migrating-v2-to-v3 diff --git a/docs/misc/extension-grants.rst b/docs/misc/extension-grants.rst index 1fbe55a2..4ce22bfd 100644 --- a/docs/misc/extension-grants.rst +++ b/docs/misc/extension-grants.rst @@ -6,7 +6,7 @@ Create a subclass of ``AbstractGrantType`` and create methods `handle` and `save .. code-block:: js - const OAuth2Server = require('oauth2-server'); + const OAuth2Server = require('@node-oauth/oauth2-server'); const AbstractGrantType = OAuth2Server.AbstractGrantType; const InvalidArgumentError = OAuth2Server.InvalidArgumentError; const InvalidRequestError = OAuth2Server.InvalidRequestError; diff --git a/docs/misc/migrating-to-v5.rst b/docs/misc/migrating-to-v5.rst new file mode 100644 index 00000000..279c343b --- /dev/null +++ b/docs/misc/migrating-to-v5.rst @@ -0,0 +1,44 @@ +=========================== + Migrating to 5.x +=========================== + +This guide covers the most breaking changes, in case you updated from an earlier version. + +------------------- +Requires Node >= 16 +------------------- + +Due to Node 14 reaching end of life (EOL; which implies no security updates) this version requires at least Node 16. +Future versions of the 5.x major releases will update to a newer Node LTS, once the current one reaches EOL. + +Note, that we also won't regard any security patches to problems that are a direct consequence of +using a Node version that reached EOL. + +------------------------ +Removed callback support +------------------------ + +With beginning of release 5.0.0 this module dropped all callback support and uses `async/await` +for all asynchronous operations. + +This implies you either need to have a more recent Node.js environment that natively supports `async/await` +or your project uses tools to support at least Promises. + +----------------- +Update your model +----------------- + +The model functions is now expected to return a Promise (or being declared as `async function`), +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 +------------------ + +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. + +This implies to requests, responses and model implementations where scope is included. diff --git a/docs/misc/migrating-v2-to-v3.rst b/docs/misc/migrating-v2-to-v3.rst index 9d03c8f2..8d1290c0 100644 --- a/docs/misc/migrating-v2-to-v3.rst +++ b/docs/misc/migrating-v2-to-v3.rst @@ -11,7 +11,7 @@ Middlewares The naming of the exposed middlewares has changed to match the OAuth2 _RFC_ more closely. Please refer to the table below: +-------------------+------------------------------------------------+ -| oauth2-server 2.x | oauth2-server 3.x | +| oauth2-server 2.x | @node-oauth/oauth2-server 3.x | +===================+================================================+ | authorise | authenticate | +-------------------+------------------------------------------------+ diff --git a/docs/misc/pkce.rst b/docs/misc/pkce.rst new file mode 100644 index 00000000..cb52f1e7 --- /dev/null +++ b/docs/misc/pkce.rst @@ -0,0 +1,141 @@ +================ + PKCE Support +================ + +Starting with release 4.3.0_ this library supports PKCE (Proof Key for Code Exchange by OAuth Public Clients) as +defined in :rfc:`7636`. + +.. _4.3.0: https://github.com/node-oauth/node-oauth2-server/releases/tag/v4.3.0 + +The PKCE integrates only with the :ref:`authorization code `. The abstract workflow looks like +the following: + +:: + + +-------------------+ + | Authz Server | + +--------+ | +---------------+ | + | |--(A)- Authorization Request ---->| | | + | | + t(code_verifier), t_m | | Authorization | | + | | | | Endpoint | | + | |<-(B)---- Authorization Code -----| | | + | | | +---------------+ | + | Client | | | + | | | +---------------+ | + | |--(C)-- Access Token Request ---->| | | + | | + code_verifier | | Token | | + | | | | Endpoint | | + | |<-(D)------ Access Token ---------| | | + +--------+ | +---------------+ | + +-------------------+ + + Figure 2: Abstract Protocol Flow + +See :rfc:`Section 1 of RFC 7636 <7636#section-1.1>`. + +1. Authorization request +======================== + +.. _PKCE#authorizationRequest: + + A. The client creates and records a secret named the "code_verifier" and derives a transformed version "t(code_verifier)" (referred to as the "code_challenge"), which is sent in the OAuth 2.0 Authorization Request along with the transformation method "t_m". + +The following shows an example of how a client could generate a `code_challenge`` and +``code_challenge_method`` for the authorizazion request. + +:: + + const base64URLEncode = str => str.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + + // This is the code_verifier, which is INITIALLY KEPT SECRET on the client + // and which is later passed as request param to the token endpoint. + // DO NOT SEND this with the authorization request! + const codeVerifier = base64URLEncode(crypto.randomBytes(32)) + + // This is the hashed version of the verifier, which is sent to the authorization endpoint. + // This is named t(code_verifier) in the above workflow + // Send this with the authorization request! + const codeChallenge = base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) + + // This is the name of the code challenge method + // This is named t_m in the above workflow + // Send this with the authorization request! + const codeChallengeMethod = 'S256' + + // add these to the request that is fired from the client + +In this project the authorize endpoint calls OAuth2Server.prototype.authorize which itself uses AuthorizeHandler. +If your Request body contains code_challenge and code_challenge_method then PKCE is active. + +:: + + const server = new OAuth2Server({ model }) + + // this could be added to express or other middleware + const authorizeEndpoint = function (req, res, next) { + const request = new Request(req) + req.query.code_challenge // the codeChallenge value + req.query.code_challenge_method // 'S256' + + server.authorize(request, response, options) + .then(function (code) { + // add code to response, code should not contain + // code_challenge or code_challenge_method + }) + .catch(function (err) { + // handle error condition + }) + } + +2. Authorization response +========================= + +.. _PKCE#authorizationResponse: + + B. The Authorization Endpoint responds as usual but records "t(code_verifier)" and the transformation method. + +The ``AuthorizeHandler.handle`` saves code challenge and code challenge method automatically via ``model.saveAuthorizationCode``. +Note that this calls your model with additional properties ``code.codeChallenge`` and ``code.codeChallengeMethod``. + + +3. Access Token Request +======================= + +.. _PKCE#accessTokenRequest: + + C. The client then sends the authorization code in the Access Token Request as usual but includes the "code_verifier" secret generated at (A). + +This is usually done in your token endpoint, that uses ``OAuth2Server.token``. + +:: + + const server = new OAuth2Server({ model }) + + // ...authorizeEndpoint + + // this could be added to express or other middleware + const tokenEndpoint = function (req, res, next) { + const request = new Request(req) + request.body.code_verifier // the non-hashed code verifier + server.token(request, response, options) + .then(function (code) { + // add code to response, code should contain + }) + .catch(function (err) { + // handle error condition + }) + } + +Note that your client should have kept ``code_verifier`` a secret until this step and now includes it as param for the token endpoint call. + + + D. The authorization server transforms "code_verifier" and compares it to "t(code_verifier)" from (B). Access is denied if they are not equal. + +This will call ``model.getAuthorizationCode`` to load the code. +The loaded code has to contain ``codeChallenge`` and ``codeChallengeMethod``. +If ``model.saveAuthorizationCode`` did not cover these values when saving the code then this step will deny the request. + +See :ref:`Model#saveAuthorizationCode` and :ref:`Model#getAuthorizationCode` diff --git a/docs/model/spec.rst b/docs/model/spec.rst index 953c2811..f18923f8 100644 --- a/docs/model/spec.rst +++ b/docs/model/spec.rst @@ -2,7 +2,9 @@ Model Specification ===================== -Each model function supports *promises*, *Node-style callbacks*, *ES6 generators* and *async*/*await* (using Babel_). Note that promise support implies support for returning plain values where asynchronism is not required. +**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_). Note that promise support implies support for returning plain values where asynchronism is not required. .. _Babel: https://babeljs.io @@ -14,9 +16,9 @@ Each model function supports *promises*, *Node-style callbacks*, *ES6 generators return new Promise('works!'); }, - // Or, calling a Node-style callback. - getAuthorizationCode: function(done) { - done(null, 'works!'); + // Or sync-style values + getAuthorizationCode: function() { + return 'works!' }, // Or, using generators. @@ -32,7 +34,7 @@ Each model function supports *promises*, *Node-style callbacks*, *ES6 generators } }; - const OAuth2Server = require('oauth2-server'); + const OAuth2Server = require('@node-oauth/oauth2-server'); let oauth = new OAuth2Server({model: model}); Code examples on this page use *promises*. @@ -41,7 +43,7 @@ Code examples on this page use *promises*. .. _Model#generateAccessToken: -``generateAccessToken(client, user, scope, [callback])`` +``generateAccessToken(client, user, scope)`` ======================================================== Invoked to generate a new access token. @@ -64,9 +66,7 @@ This model function is **optional**. If not implemented, a default handler is us +------------+----------+---------------------------------------------------------------------+ | user | Object | The user the access token is generated for. | +------------+----------+---------------------------------------------------------------------+ -| scope | String | The scopes associated with the access token. Can be ``null``. | -+------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | +| scope | String[] | The scopes associated with the access token. Can be ``null``. | +------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -85,7 +85,7 @@ A ``String`` to be used as access token. .. _Model#generateRefreshToken: -``generateRefreshToken(client, user, scope, [callback])`` +``generateRefreshToken(client, user, scope)`` ========================================================= Invoked to generate a new refresh token. @@ -107,9 +107,7 @@ This model function is **optional**. If not implemented, a default handler is us +------------+----------+---------------------------------------------------------------------+ | user | Object | The user the refresh token is generated for. | +------------+----------+---------------------------------------------------------------------+ -| scope | String | The scopes associated with the refresh token. Can be ``null``. | -+------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | +| scope | String[] | The scopes associated with the refresh token. Can be ``null``. | +------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -128,7 +126,7 @@ A ``String`` to be used as refresh token. .. _Model#generateAuthorizationCode: -``generateAuthorizationCode(client, user, scope, [callback])`` +``generateAuthorizationCode(client, user, scope)`` ========================================= Invoked to generate a new authorization code. @@ -148,9 +146,7 @@ This model function is **optional**. If not implemented, a default handler is us +------------+----------+---------------------------------------------------------------------+ | user | Object | The user the authorization code is generated for. | +------------+----------+---------------------------------------------------------------------+ -| scope | String | The scopes associated with the authorization code. Can be ``null``. | -+------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | +| scope | String[] | The scopes associated with the authorization code. Can be ``null``. | +------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -163,7 +159,7 @@ A ``String`` to be used as authorization code. .. _Model#getAccessToken: -``getAccessToken(accessToken, [callback])`` +``getAccessToken(accessToken)`` =========================================== Invoked to retrieve an existing access token previously saved through :ref:`Model#saveToken() `. @@ -181,30 +177,28 @@ This model function is **required** if :ref:`OAuth2Server#authenticate() `. @@ -255,30 +249,28 @@ This model function is **required** if the ``refresh_token`` grant is used. +==============+==========+=====================================================================+ | refreshToken | String | The access token to retrieve. | +--------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+--------------+----------+---------------------------------------------------------------------+ **Return value:** An ``Object`` representing the refresh token and associated data. -+-------------------------------+--------+----------------------------------------------------+ -| Name | Type | Description | -+===============================+========+====================================================+ -| token | Object | The return value. | -+-------------------------------+--------+----------------------------------------------------+ -| token.refreshToken | String | The refresh token passed to ``getRefreshToken()``. | -+-------------------------------+--------+----------------------------------------------------+ -| [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | -+-------------------------------+--------+----------------------------------------------------+ -| [token.scope] | String | The authorized scope of the refresh token. | -+-------------------------------+--------+----------------------------------------------------+ -| token.client | Object | The client associated with the refresh token. | -+-------------------------------+--------+----------------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+-------------------------------+--------+----------------------------------------------------+ -| token.user | Object | The user associated with the refresh token. | -+-------------------------------+--------+----------------------------------------------------+ ++-------------------------------+----------+----------------------------------------------------+ +| Name | Type | Description | ++===============================+==========+====================================================+ +| token | Object | The return value. | ++-------------------------------+----------+----------------------------------------------------+ +| token.refreshToken | String | The refresh token passed to ``getRefreshToken()``. | ++-------------------------------+----------+----------------------------------------------------+ +| [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | ++-------------------------------+----------+----------------------------------------------------+ +| [token.scope] | String[] | The authorized scope of the refresh token. | ++-------------------------------+----------+----------------------------------------------------+ +| token.client | Object | The client associated with the refresh token. | ++-------------------------------+----------+----------------------------------------------------+ +| token.client.id | String | A unique string identifying the client. | ++-------------------------------+----------+----------------------------------------------------+ +| token.user | Object | The user associated with the refresh token. | ++-------------------------------+----------+----------------------------------------------------+ ``token.client`` and ``token.user`` can carry additional properties that will be ignored by *oauth2-server*. @@ -311,7 +303,7 @@ An ``Object`` representing the refresh token and associated data. .. _Model#getAuthorizationCode: -``getAuthorizationCode(authorizationCode, [callback])`` +``getAuthorizationCode(authorizationCode)`` ======================================================= Invoked to retrieve an existing authorization code previously saved through :ref:`Model#saveAuthorizationCode() `. @@ -329,32 +321,30 @@ This model function is **required** if the ``authorization_code`` grant is used. +===================+==========+=====================================================================+ | authorizationCode | String | The authorization code to retrieve. | +-------------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+-------------------+----------+---------------------------------------------------------------------+ **Return value:** An ``Object`` representing the authorization code and associated data. -+--------------------+--------+--------------------------------------------------------------+ -| Name | Type | Description | -+====================+========+==============================================================+ -| code | Object | The return value. | -+--------------------+--------+--------------------------------------------------------------+ -| code.code | String | The authorization code passed to ``getAuthorizationCode()``. | -+--------------------+--------+--------------------------------------------------------------+ -| code.expiresAt | Date | The expiry time of the authorization code. | -+--------------------+--------+--------------------------------------------------------------+ -| [code.redirectUri] | String | The redirect URI of the authorization code. | -+--------------------+--------+--------------------------------------------------------------+ -| [code.scope] | String | The authorized scope of the authorization code. | -+--------------------+--------+--------------------------------------------------------------+ -| code.client | Object | The client associated with the authorization code. | -+--------------------+--------+--------------------------------------------------------------+ -| code.client.id | String | A unique string identifying the client. | -+--------------------+--------+--------------------------------------------------------------+ -| code.user | Object | The user associated with the authorization code. | -+--------------------+--------+--------------------------------------------------------------+ ++--------------------+----------+--------------------------------------------------------------+ +| Name | Type | Description | ++====================+==========+==============================================================+ +| code | Object | The return value. | ++--------------------+----------+--------------------------------------------------------------+ +| code.code | String | The authorization code passed to ``getAuthorizationCode()``. | ++--------------------+----------+--------------------------------------------------------------+ +| code.expiresAt | Date | The expiry time of the authorization code. | ++--------------------+----------+--------------------------------------------------------------+ +| [code.redirectUri] | String | The redirect URI of the authorization code. | ++--------------------+----------+--------------------------------------------------------------+ +| [code.scope] | String[] | The authorized scope of the authorization code. | ++--------------------+----------+--------------------------------------------------------------+ +| code.client | Object | The client associated with the authorization code. | ++--------------------+----------+--------------------------------------------------------------+ +| code.client.id | String | A unique string identifying the client. | ++--------------------+----------+--------------------------------------------------------------+ +| code.user | Object | The user associated with the authorization code. | ++--------------------+----------+--------------------------------------------------------------+ ``code.client`` and ``code.user`` can carry additional properties that will be ignored by *oauth2-server*. @@ -374,7 +364,7 @@ An ``Object`` representing the authorization code and associated data. }) .spread(function(code, client, user) { return { - code: code.authorization_code, + authorizationCode: code.authorization_code, expiresAt: code.expires_at, redirectUri: code.redirect_uri, scope: code.scope, @@ -388,7 +378,7 @@ An ``Object`` representing the authorization code and associated data. .. _Model#getClient: -``getClient(clientId, clientSecret, [callback])`` +``getClient(clientId, clientSecret)`` ================================================= Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. @@ -411,8 +401,6 @@ This model function is **required** for all grant types. +--------------+----------+---------------------------------------------------------------------+ | clientSecret | String | The client secret of the client to retrieve. Can be ``null``. | +--------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+--------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -460,7 +448,7 @@ The return value (``client``) can carry additional properties that will be ignor .. _Model#getUser: -``getUser(username, password, [callback])`` +``getUser(username, password, client)`` =========================================== Invoked to retrieve a user using a username/password combination. @@ -473,15 +461,15 @@ This model function is **required** if the ``password`` grant is used. **Arguments:** -+------------+----------+---------------------------------------------------------------------+ -| Name | Type | Description | -+============+==========+=====================================================================+ -| username | String | The username of the user to retrieve. | -+------------+----------+---------------------------------------------------------------------+ -| password | String | The user's password. | -+------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+------------+----------+---------------------------------------------------------------------+ ++-------------------+----------+---------------------------------------------------------------------+ +| Name | Type | Description | ++===================+==========+=====================================================================+ +| username | String | The username of the user to retrieve. | ++-------------------+----------+---------------------------------------------------------------------+ +| password | String | The user's password. | ++-------------------+----------+---------------------------------------------------------------------+ +| client (optional) | Client | The client. | ++-------------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -500,7 +488,7 @@ An ``Object`` representing the user, or a falsy value if no such user could be f .. _Model#getUserFromClient: -``getUserFromClient(client, [callback])`` +``getUserFromClient(client)`` ========================================= Invoked to retrieve the user associated with the specified client. @@ -520,8 +508,6 @@ This model function is **required** if the ``client_credentials`` grant is used. +------------+----------+---------------------------------------------------------------------+ | client.id | String | A unique string identifying the client. | +------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -542,7 +528,7 @@ An ``Object`` representing the user, or a falsy value if the client does not hav .. _Model#saveToken: -``saveToken(token, client, user, [callback])`` +``saveToken(token, client, user)`` ============================================== Invoked to save an access token and optionally a refresh token, depending on the grant type. @@ -571,40 +557,38 @@ This model function is **required** for all grant types. +-------------------------------+----------+---------------------------------------------------------------------+ | [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | +-------------------------------+----------+---------------------------------------------------------------------+ -| [token.scope] | String | The authorized scope of the token(s). | +| [token.scope] | Stringp[] | The authorized scope of the token(s). | +-------------------------------+----------+---------------------------------------------------------------------+ | client | Object | The client associated with the token(s). | +-------------------------------+----------+---------------------------------------------------------------------+ | user | Object | The user associated with the token(s). | +-------------------------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+-------------------------------+----------+---------------------------------------------------------------------+ **Return value:** An ``Object`` representing the token(s) and associated data. -+-----------------------------+--------+----------------------------------------------+ -| Name | Type | Description | -+=============================+========+==============================================+ -| token | Object | The return value. | -+-----------------------------+--------+----------------------------------------------+ -| token.accessToken | String | The access token passed to ``saveToken()``. | -+-----------------------------+--------+----------------------------------------------+ -| token.accessTokenExpiresAt | Date | The expiry time of the access token. | -+-----------------------------+--------+----------------------------------------------+ -| token.refreshToken | String | The refresh token passed to ``saveToken()``. | -+-----------------------------+--------+----------------------------------------------+ -| token.refreshTokenExpiresAt | Date | The expiry time of the refresh token. | -+-----------------------------+--------+----------------------------------------------+ -| [token.scope] | String | The authorized scope of the access token. | -+-----------------------------+--------+----------------------------------------------+ -| token.client | Object | The client associated with the access token. | -+-----------------------------+--------+----------------------------------------------+ -| token.client.id | String | A unique string identifying the client. | -+-----------------------------+--------+----------------------------------------------+ -| token.user | Object | The user associated with the access token. | -+-----------------------------+--------+----------------------------------------------+ ++-----------------------------+----------+----------------------------------------------+ +| Name | Type | Description | ++=============================+==========+==============================================+ +| token | Object | The return value. | ++-----------------------------+----------+----------------------------------------------+ +| token.accessToken | String | The access token passed to ``saveToken()``. | ++-----------------------------+----------+----------------------------------------------+ +| token.accessTokenExpiresAt | Date | The expiry time of the access token. | ++-----------------------------+----------+----------------------------------------------+ +| token.refreshToken | String | The refresh token passed to ``saveToken()``. | ++-----------------------------+----------+----------------------------------------------+ +| token.refreshTokenExpiresAt | Date | The expiry time of the refresh token. | ++-----------------------------+----------+----------------------------------------------+ +| [token.scope] | String[] | The authorized scope of the access token. | ++-----------------------------+----------+----------------------------------------------+ +| token.client | Object | The client associated with the access token. | ++-----------------------------+----------+----------------------------------------------+ +| token.client.id | String | A unique string identifying the client. | ++-----------------------------+----------+----------------------------------------------+ +| token.user | Object | The user associated with the access token. | ++-----------------------------+----------+----------------------------------------------+ ``token.client`` and ``token.user`` can carry additional properties that will be ignored by *oauth2-server*. @@ -650,7 +634,7 @@ If the ``allowExtendedTokenAttributes`` server option is enabled (see :ref:`OAut .. _Model#saveAuthorizationCode: -``saveAuthorizationCode(code, client, user, [callback])`` +``saveAuthorizationCode(code, client, user)`` ========================================================= Invoked to save an authorization code. @@ -674,14 +658,12 @@ This model function is **required** if the ``authorization_code`` grant is used. +------------------------+----------+---------------------------------------------------------------------+ | code.redirectUri | String | The redirect URI associated with the authorization code. | +------------------------+----------+---------------------------------------------------------------------+ -| [code.scope] | String | The authorized scope of the authorization code. | +| [code.scope] | String[] | The authorized scope of the authorization code. | +------------------------+----------+---------------------------------------------------------------------+ | client | Object | The client associated with the authorization code. | +------------------------+----------+---------------------------------------------------------------------+ | user | Object | The user associated with the authorization code. | +------------------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+------------------------+----------+---------------------------------------------------------------------+ .. todo:: Is ``code.scope`` really optional? @@ -689,25 +671,25 @@ This model function is **required** if the ``authorization_code`` grant is used. An ``Object`` representing the authorization code and associated data. -+------------------------+--------+---------------------------------------------------------------+ -| Name | Type | Description | -+========================+========+===============================================================+ -| code | Object | The return value. | -+------------------------+--------+---------------------------------------------------------------+ -| code.authorizationCode | String | The authorization code passed to ``saveAuthorizationCode()``. | -+------------------------+--------+---------------------------------------------------------------+ -| code.expiresAt | Date | The expiry time of the authorization code. | -+------------------------+--------+---------------------------------------------------------------+ -| code.redirectUri | String | The redirect URI associated with the authorization code. | -+------------------------+--------+---------------------------------------------------------------+ -| [code.scope] | String | The authorized scope of the authorization code. | -+------------------------+--------+---------------------------------------------------------------+ -| code.client | Object | The client associated with the authorization code. | -+------------------------+--------+---------------------------------------------------------------+ -| code.client.id | String | A unique string identifying the client. | -+------------------------+--------+---------------------------------------------------------------+ -| code.user | Object | The user associated with the authorization code. | -+------------------------+--------+---------------------------------------------------------------+ ++------------------------+----------+---------------------------------------------------------------+ +| Name | Type | Description | ++========================+==========+===============================================================+ +| code | Object | The return value. | ++------------------------+----------+---------------------------------------------------------------+ +| code.authorizationCode | String | The authorization code passed to ``saveAuthorizationCode()``. | ++------------------------+----------+---------------------------------------------------------------+ +| code.expiresAt | Date | The expiry time of the authorization code. | ++------------------------+----------+---------------------------------------------------------------+ +| code.redirectUri | String | The redirect URI associated with the authorization code. | ++------------------------+----------+---------------------------------------------------------------+ +| [code.scope] | String[] | The authorized scope of the authorization code. | ++------------------------+----------+---------------------------------------------------------------+ +| code.client | Object | The client associated with the authorization code. | ++------------------------+----------+---------------------------------------------------------------+ +| code.client.id | String | A unique string identifying the client. | ++------------------------+----------+---------------------------------------------------------------+ +| code.user | Object | The user associated with the authorization code. | ++------------------------+----------+---------------------------------------------------------------+ ``code.client`` and ``code.user`` can carry additional properties that will be ignored by *oauth2-server*. @@ -742,7 +724,7 @@ An ``Object`` representing the authorization code and associated data. .. _Model#revokeToken: -``revokeToken(token, [callback])`` +``revokeToken(token)`` ================================== Invoked to revoke a refresh token. @@ -764,7 +746,7 @@ This model function is **required** if the ``refresh_token`` grant is used. +-------------------------------+----------+---------------------------------------------------------------------+ | [token.refreshTokenExpiresAt] | Date | The expiry time of the refresh token. | +-------------------------------+----------+---------------------------------------------------------------------+ -| [token.scope] | String | The authorized scope of the refresh token. | +| [token.scope] | String[] | The authorized scope of the refresh token. | +-------------------------------+----------+---------------------------------------------------------------------+ | token.client | Object | The client associated with the refresh token. | +-------------------------------+----------+---------------------------------------------------------------------+ @@ -772,8 +754,6 @@ This model function is **required** if the ``refresh_token`` grant is used. +-------------------------------+----------+---------------------------------------------------------------------+ | token.user | Object | The user associated with the refresh token. | +-------------------------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+-------------------------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -797,7 +777,7 @@ Return ``true`` if the revocation was successful or ``false`` if the refresh tok .. _Model#revokeAuthorizationCode: -``revokeAuthorizationCode(code, [callback])`` +``revokeAuthorizationCode(code)`` ============================================= Invoked to revoke an authorization code. @@ -821,7 +801,7 @@ This model function is **required** if the ``authorization_code`` grant is used. +--------------------+----------+---------------------------------------------------------------------+ | [code.redirectUri] | String | The redirect URI of the authorization code. | +--------------------+----------+---------------------------------------------------------------------+ -| [code.scope] | String | The authorized scope of the authorization code. | +| [code.scope] | String[] | The authorized scope of the authorization code. | +--------------------+----------+---------------------------------------------------------------------+ | code.client | Object | The client associated with the authorization code. | +--------------------+----------+---------------------------------------------------------------------+ @@ -829,8 +809,6 @@ This model function is **required** if the ``authorization_code`` grant is used. +--------------------+----------+---------------------------------------------------------------------+ | code.user | Object | The user associated with the authorization code. | +--------------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+--------------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -854,7 +832,7 @@ Return ``true`` if the revocation was successful or ``false`` if the authorizati .. _Model#validateScope: -``validateScope(user, client, scope, [callback])`` +``validateScope(user, client, scope)`` ================================================== Invoked to check if the requested ``scope`` is valid for a particular ``client``/``user`` combination. @@ -878,9 +856,7 @@ This model function is **optional**. If not implemented, any scope is accepted. +------------+----------+---------------------------------------------------------------------+ | client.id | Object | A unique string identifying the client. | +------------+----------+---------------------------------------------------------------------+ -| scope | String | The scopes to validate. | -+------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | +| scope | String[] | The scopes to validate. | +------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -903,7 +879,7 @@ To reject invalid or only partially valid scopes: const VALID_SCOPES = ['read', 'write']; function validateScope(user, client, scope) { - if (!scope.split(' ').every(s => VALID_SCOPES.indexOf(s) >= 0)) { + if (!scope.every(s => VALID_SCOPES.indexOf(s) >= 0)) { return false; } return scope; @@ -917,24 +893,20 @@ To accept partially valid scopes: const VALID_SCOPES = ['read', 'write']; function validateScope(user, client, scope) { - return scope - .split(' ') - .filter(s => VALID_SCOPES.indexOf(s) >= 0) - .join(' '); + return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0); } -Note that the example above will still reject completely invalid scopes, since ``validateScope`` returns an empty string if all scopes are filtered out. - -------- .. _Model#verifyScope: -``verifyScope(accessToken, scope, [callback])`` +``verifyScope(accessToken, scope)`` =============================================== 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 :ref:`OAuth2Server#authenticate() `. +This model function is **required** if scopes are used with :ref:`OAuth2Server#authenticate() ` +but it's never called, if you provide your own ``authenticateHandler`` to the options. **Invoked during:** @@ -951,7 +923,7 @@ This model function is **required** if scopes are used with :ref:`OAuth2Server#a +------------------------------+----------+---------------------------------------------------------------------+ | [token.accessTokenExpiresAt] | Date | The expiry time of the access token. | +------------------------------+----------+---------------------------------------------------------------------+ -| [token.scope] | String | The authorized scope of the access token. | +| [token.scope] | String[] | The authorized scope of the access token. | +------------------------------+----------+---------------------------------------------------------------------+ | token.client | Object | The client associated with the access token. | +------------------------------+----------+---------------------------------------------------------------------+ @@ -959,9 +931,7 @@ This model function is **required** if scopes are used with :ref:`OAuth2Server#a +------------------------------+----------+---------------------------------------------------------------------+ | token.user | Object | The user associated with the access token. | +------------------------------+----------+---------------------------------------------------------------------+ -| scope | String | The required scopes. | -+------------------------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | +| scope | String[] | The required scopes. | +------------------------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -976,20 +946,19 @@ Returns ``true`` if the access token passes, ``false`` otherwise. :: - function verifyScope(token, scope) { + function verifyScope(token, requestedScopes) { if (!token.scope) { return false; } - let requestedScopes = scope.split(' '); - let authorizedScopes = token.scope.split(' '); - return requestedScopes.every(s => authorizedScopes.indexOf(s) >= 0); + let authorizedScopes = token.scope; + return requestedScopes.every(s => token.scope.includes(scope)); } -------- .. _Model#validateRedirectUri: -``validateRedirectUri(redirectUri, client, [callback])`` +``validateRedirectUri(redirectUri, client)`` ================================================================ Invoked to check if the provided ``redirectUri`` is valid for a particular ``client``. diff --git a/docs/npm_conf.py b/docs/npm_conf.py index f86915f5..41b03819 100644 --- a/docs/npm_conf.py +++ b/docs/npm_conf.py @@ -40,10 +40,10 @@ def get_config(): 'name': package['name'], 'version': package['version'], 'short_version': get_short_version(package['version']), - 'organization': 'oauthjs', + 'organization': '@node-oauth', 'copyright_year': get_copyright_year(2016), # TODO: Get authors from package. - 'docs_author': 'Max Truxa', - 'docs_author_email': 'dev@maxtruxa.com' + 'docs_author': 'Node-OAuth Authors', + 'docs_author_email': '' } diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..1ee13a2b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +# Defining the exact version will make sure things don't break +sphinx==5.3.0 +sphinx_rtd_theme==1.1.1 +readthedocs-sphinx-search==0.1.1 \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 7fb609e3..5cd73d9c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -// Type definitions for Node OAuth2 Server 4.0 +// Type definitions for Node OAuth2 Server 5.0 // Definitions by: Robbie Van Gorkom , // Charles Irick , // Daniel Fischer , @@ -23,8 +23,7 @@ declare class OAuth2Server { authenticate( request: OAuth2Server.Request, response: OAuth2Server.Response, - options?: OAuth2Server.AuthenticateOptions, - callback?: OAuth2Server.Callback + options?: OAuth2Server.AuthenticateOptions ): Promise; /** @@ -33,8 +32,7 @@ declare class OAuth2Server { authorize( request: OAuth2Server.Request, response: OAuth2Server.Response, - options?: OAuth2Server.AuthorizeOptions, - callback?: OAuth2Server.Callback + options?: OAuth2Server.AuthorizeOptions ): Promise; /** @@ -43,8 +41,7 @@ declare class OAuth2Server { token( request: OAuth2Server.Request, response: OAuth2Server.Response, - options?: OAuth2Server.TokenOptions, - callback?: OAuth2Server.Callback + options?: OAuth2Server.TokenOptions ): Promise; } @@ -121,13 +118,13 @@ declare namespace OAuth2Server { * Generate access token. Calls Model#generateAccessToken() if implemented. * */ - generateAccessToken(client: Client, user: User, scope: string | string[]): Promise; + generateAccessToken(client: Client, user: User, scope: string[]): Promise; /** * Generate refresh token. Calls Model#generateRefreshToken() if implemented. * */ - generateRefreshToken(client: Client, user: User, scope: string | string[]): Promise; + generateRefreshToken(client: Client, user: User, scope: string[]): Promise; /** * Get access token expiration date. @@ -145,13 +142,13 @@ declare namespace OAuth2Server { * Get scope from the request body. * */ - getScope(request: Request): string; + getScope(request: Request): string[]; /** * Validate requested scope. Calls Model#validateScope() if implemented. * */ - validateScope(user: User, client: Client, scope: string | string[]): Promise; + validateScope(user: User, client: Client, scope: string[]): Promise; /** * Retrieve info from the request and client and return token @@ -171,7 +168,7 @@ declare namespace OAuth2Server { /** * The scope(s) to authenticate. */ - scope?: string | string[] | undefined; + scope?: string[] | undefined; /** * Set the X-Accepted-OAuth-Scopes HTTP header on response objects. @@ -238,11 +235,6 @@ declare namespace OAuth2Server { extendedGrantTypes?: { [key: string]: typeof AbstractGrantType } | undefined; } - /** - * Represents a generic callback structure for model callbacks - */ - type Callback = (err?: any, result?: T) => void; - /** * For returning falsey parameters in cases of failure */ @@ -253,19 +245,19 @@ declare namespace OAuth2Server { * Invoked to generate a new access token. * */ - generateAccessToken?(client: Client, user: User, scope: string | string[], callback?: Callback): Promise; + generateAccessToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. * */ - getClient(clientId: string, clientSecret: string, callback?: Callback): Promise; + getClient(clientId: string, clientSecret: string): Promise; /** * Invoked to save an access token and optionally a refresh token, depending on the grant type. * */ - saveToken(token: Token, client: Client, user: User, callback?: Callback): Promise; + saveToken(token: Token, client: Client, user: User): Promise; } interface RequestAuthenticationModel { @@ -273,14 +265,14 @@ declare namespace OAuth2Server { * Invoked to retrieve an existing access token previously saved through Model#saveToken(). * */ - getAccessToken(accessToken: string, callback?: Callback): Promise; + getAccessToken(accessToken: string): Promise; /** * Invoked during request authentication to check if the provided access token was authorized the requested scopes. * Optional, if a custom authenticateHandler is used or if there is no scope part of the request. * */ - verifyScope?(token: Token, scope: string | string[], callback?: Callback): Promise; + verifyScope?(token: Token, scope: string[]): Promise; } interface AuthorizationCodeModel extends BaseModel, RequestAuthenticationModel { @@ -288,19 +280,19 @@ declare namespace OAuth2Server { * Invoked to generate a new refresh token. * */ - generateRefreshToken?(client: Client, user: User, scope: string | string[], callback?: Callback): Promise; + generateRefreshToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to generate a new authorization code. * */ - generateAuthorizationCode?(client: Client, user: User, scope: string | string[], callback?: Callback): Promise; + generateAuthorizationCode?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve an existing authorization code previously saved through Model#saveAuthorizationCode(). * */ - getAuthorizationCode(authorizationCode: string, callback?: Callback): Promise; + getAuthorizationCode(authorizationCode: string): Promise; /** * Invoked to save an authorization code. @@ -309,21 +301,21 @@ declare namespace OAuth2Server { saveAuthorizationCode( code: Pick, client: Client, - user: User, - callback?: Callback): Promise; + user: User + ): Promise; /** * Invoked to revoke an authorization code. * */ - revokeAuthorizationCode(code: AuthorizationCode, callback?: Callback): Promise; + revokeAuthorizationCode(code: AuthorizationCode): Promise; /** * Invoked to check if the requested scope is valid for a particular client/user combination. * */ - validateScope?(user: User, client: Client, scope: string | string[], callback?: Callback): Promise; - + validateScope?(user: User, client: Client, scope: string[]): Promise; + /** * Invoked to check if the provided `redirectUri` is valid for a particular `client`. * @@ -336,19 +328,19 @@ declare namespace OAuth2Server { * Invoked to generate a new refresh token. * */ - generateRefreshToken?(client: Client, user: User, scope: string | string[], callback?: Callback): Promise; + generateRefreshToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve a user using a username/password combination. * */ - getUser(username: string, password: string, callback?: Callback): Promise; + getUser(username: string, password: string, client: Client): Promise; /** * Invoked to check if the requested scope is valid for a particular client/user combination. * */ - validateScope?(user: User, client: Client, scope: string | string[], callback?: Callback): Promise; + validateScope?(user: User, client: Client, scope: string[]): Promise; } interface RefreshTokenModel extends BaseModel, RequestAuthenticationModel { @@ -356,19 +348,19 @@ declare namespace OAuth2Server { * Invoked to generate a new refresh token. * */ - generateRefreshToken?(client: Client, user: User, scope: string | string[], callback?: Callback): Promise; + generateRefreshToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve an existing refresh token previously saved through Model#saveToken(). * */ - getRefreshToken(refreshToken: string, callback?: Callback): Promise; + getRefreshToken(refreshToken: string): Promise; /** * Invoked to revoke a refresh token. * */ - revokeToken(token: RefreshToken | Token, callback?: Callback): Promise; + revokeToken(token: RefreshToken): Promise; } interface ClientCredentialsModel extends BaseModel, RequestAuthenticationModel { @@ -376,13 +368,13 @@ declare namespace OAuth2Server { * Invoked to retrieve the user associated with the specified client. * */ - getUserFromClient(client: Client, callback?: Callback): Promise; + getUserFromClient(client: Client): Promise; /** * Invoked to check if the requested scope is valid for a particular client/user combination. * */ - validateScope?(user: User, client: Client, scope: string | string[], callback?: Callback): Promise; + validateScope?(user: User, client: Client, scope: string[]): Promise; } interface ExtensionModel extends BaseModel, RequestAuthenticationModel {} @@ -414,7 +406,7 @@ declare namespace OAuth2Server { authorizationCode: string; expiresAt: Date; redirectUri: string; - scope?: string | string[] | undefined; + scope?: string[] | undefined; client: Client; user: User; codeChallenge?: string; @@ -430,7 +422,7 @@ declare namespace OAuth2Server { accessTokenExpiresAt?: Date | undefined; refreshToken?: string | undefined; refreshTokenExpiresAt?: Date | undefined; - scope?: string | string[] | undefined; + scope?: string[] | undefined; client: Client; user: User; [key: string]: any; @@ -442,7 +434,7 @@ declare namespace OAuth2Server { interface RefreshToken { refreshToken: string; refreshTokenExpiresAt?: Date | undefined; - scope?: string | string[] | undefined; + scope?: string[] | undefined; client: Client; user: User; [key: string]: any; diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index e351d940..bce24ed8 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -6,8 +6,8 @@ const InvalidArgumentError = require('../errors/invalid-argument-error'); const InvalidScopeError = require('../errors/invalid-scope-error'); -const isFormat = require('@node-oauth/formats'); const tokenUtil = require('../utils/token-util'); +const { parseScope } = require('../utils/scope-util'); class AbstractGrantType { constructor (options) { @@ -32,8 +32,9 @@ class AbstractGrantType { */ async generateAccessToken (client, user, scope) { if (this.model.generateAccessToken) { - const accessToken = await this.model.generateAccessToken(client, user, scope); - return accessToken || tokenUtil.generateRandomToken(); + // We should not fall back to a random accessToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateAccessToken(client, user, scope); } return tokenUtil.generateRandomToken(); @@ -44,8 +45,9 @@ class AbstractGrantType { */ async generateRefreshToken (client, user, scope) { if (this.model.generateRefreshToken) { - const refreshToken = await this.model.generateRefreshToken(client, user, scope); - return refreshToken || tokenUtil.generateRandomToken(); + // We should not fall back to a random refreshToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateRefreshToken(client, user, scope); } return tokenUtil.generateRandomToken(); @@ -71,11 +73,7 @@ class AbstractGrantType { * Get scope from the request body. */ getScope (request) { - if (!isFormat.nqschar(request.body.scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); - } - - return request.body.scope; + return parseScope(request.body.scope); } /** diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 71de6c2d..766b947c 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -195,11 +195,11 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); const token = { - accessToken: accessToken, - authorizationCode: authorizationCode, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, + accessToken, + authorizationCode, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, scope: validatedScope, }; diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index dc35ea1d..fa5cd27a 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -45,7 +45,7 @@ class ClientCredentialsGrantType extends AbstractGrantType { } const scope = this.getScope(request); - const user = this.getUserFromClient(client); + const user = await this.getUserFromClient(client); return this.saveToken(user, client, scope); } @@ -73,8 +73,8 @@ class ClientCredentialsGrantType extends AbstractGrantType { const accessToken = await this.generateAccessToken(client, user, validatedScope); const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(client, user, validatedScope); const token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, + accessToken, + accessTokenExpiresAt, scope: validatedScope, }; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index 2aa78816..b09e4993 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -47,7 +47,7 @@ class PasswordGrantType extends AbstractGrantType { } const scope = this.getScope(request); - const user = await this.getUser(request); + const user = await this.getUser(request, client); return this.saveToken(user, client, scope); } @@ -56,7 +56,7 @@ class PasswordGrantType extends AbstractGrantType { * Get user using a username/password combination. */ - async getUser(request) { + async getUser(request, client) { if (!request.body.username) { throw new InvalidRequestError('Missing parameter: `username`'); } @@ -73,7 +73,7 @@ class PasswordGrantType extends AbstractGrantType { throw new InvalidRequestError('Invalid parameter: `password`'); } - const user = await this.model.getUser(request.body.username, request.body.password); + const user = await this.model.getUser(request.body.username, request.body.password, client); if (!user) { throw new InvalidGrantError('Invalid grant: user credentials are invalid'); @@ -94,10 +94,10 @@ class PasswordGrantType extends AbstractGrantType { const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); const token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, scope: validatedScope, }; diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index b9e89a27..45237dbc 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -10,6 +10,7 @@ const InvalidGrantError = require('../errors/invalid-grant-error'); const InvalidRequestError = require('../errors/invalid-request-error'); const ServerError = require('../errors/server-error'); const isFormat = require('@node-oauth/formats'); +const InvalidScopeError = require('../errors/invalid-scope-error'); /** * Constructor. @@ -55,7 +56,9 @@ class RefreshTokenGrantType extends AbstractGrantType { token = await this.getRefreshToken(request, client); token = await this.revokeToken(token); - return this.saveToken(token.user, client, token.scope); + const scope = this.getScope(request, token); + + return this.saveToken(token.user, client, scope); } /** @@ -130,9 +133,9 @@ class RefreshTokenGrantType extends AbstractGrantType { const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); const token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - scope: scope, + accessToken, + accessTokenExpiresAt, + scope, }; if (this.alwaysIssueNewRefreshToken !== false) { @@ -142,6 +145,33 @@ class RefreshTokenGrantType extends AbstractGrantType { return this.model.saveToken(token, client, user); } + + getScope (request, token) { + const requestedScope = super.getScope(request); + const originalScope = token.scope; + + if (!originalScope && !requestedScope) { + return; + } + + if (!originalScope && requestedScope) { + throw new InvalidScopeError('Invalid scope: Unable to add extra scopes'); + } + + if (!requestedScope) { + return originalScope; + } + + const valid = requestedScope.every(scope => { + return originalScope.includes(scope); + }); + + if (!valid) { + throw new InvalidScopeError('Invalid scope: Unable to add extra scopes'); + } + + return requestedScope; + } } /** diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 88d6f6d2..32251758 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -235,8 +235,6 @@ class AuthenticateHandler { if (!scope) { throw new InsufficientScopeError('Insufficient scope: authorized scope is insufficient'); } - - return scope; } /** @@ -244,12 +242,16 @@ class AuthenticateHandler { */ updateResponse (response, accessToken) { + if (accessToken.scope == null) { + return; + } + if (this.scope && this.addAcceptedScopesHeader) { - response.set('X-Accepted-OAuth-Scopes', this.scope); + response.set('X-Accepted-OAuth-Scopes', this.scope.join(' ')); } if (this.scope && this.addAuthorizedScopesHeader) { - response.set('X-OAuth-Scopes', accessToken.scope); + response.set('X-OAuth-Scopes', accessToken.scope.join(' ')); } } } diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index a98a037a..12ca72cf 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -20,6 +20,7 @@ const isFormat = require('@node-oauth/formats'); const tokenUtil = require('../utils/token-util'); const url = require('url'); const pkce = require('../pkce/pkce'); +const { parseScope } = require('../utils/scope-util'); /** * Response types. @@ -92,9 +93,9 @@ class AuthorizeHandler { throw new AccessDeniedError('Access denied: user denied access to application'); } - const requestedScope = this.getScope(request); + const requestedScope = await this.getScope(request); const validScope = await this.validateScope(user, client, requestedScope); - const authorizationCode = this.generateAuthorizationCode(client, user, validScope); + const authorizationCode = await this.generateAuthorizationCode(client, user, validScope); const ResponseType = this.getResponseType(request); const codeChallenge = this.getCodeChallenge(request); @@ -226,11 +227,7 @@ class AuthorizeHandler { getScope (request) { const scope = request.body.scope || request.query.scope; - if (!isFormat.nqschar(scope)) { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } - - return scope; + return parseScope(scope); } /** @@ -370,7 +367,7 @@ class AuthorizeHandler { } getCodeChallenge (request) { - return request.body.code_challenge; + return request.body.code_challenge || request.query.code_challenge; } /** @@ -381,7 +378,7 @@ class AuthorizeHandler { * (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) */ getCodeChallengeMethod (request) { - const algorithm = request.body.code_challenge_method; + const algorithm = request.body.code_challenge_method || request.query.code_challenge_method; if (algorithm && !pkce.isValidMethod(algorithm)) { throw new InvalidRequestError(`Invalid request: transform algorithm '${algorithm}' not supported`); @@ -390,6 +387,7 @@ class AuthorizeHandler { return algorithm || 'plain'; } } + /** * Export constructor. */ diff --git a/lib/server.js b/lib/server.js index a73acd63..a2e31878 100644 --- a/lib/server.js +++ b/lib/server.js @@ -26,14 +26,9 @@ class OAuth2Server { /** * Authenticate a token. - * Note, that callback will soon be deprecated! */ authenticate (request, response, options) { - if (typeof options === 'string') { - options = {scope: options}; - } - options = Object.assign({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, diff --git a/lib/utils/scope-util.js b/lib/utils/scope-util.js new file mode 100644 index 00000000..61278587 --- /dev/null +++ b/lib/utils/scope-util.js @@ -0,0 +1,16 @@ +const isFormat = require('@node-oauth/formats'); +const InvalidScopeError = require('../errors/invalid-scope-error'); + +module.exports = { + parseScope: function (requestedScope) { + if (!isFormat.nqschar(requestedScope)) { + throw new InvalidScopeError('Invalid parameter: `scope`'); + } + + if (requestedScope == null) { + return undefined; + } + + return requestedScope.split(' '); + } +}; diff --git a/package.json b/package.json index 44d69278..437c2476 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@node-oauth/oauth2-server", "description": "Complete, framework-agnostic, compliant and well tested module for implementing an OAuth2 Server in node.js", - "version": "4.3.0", + "version": "5.0.0", "keywords": [ "oauth", "oauth2" @@ -22,7 +22,8 @@ "files": [ "index.js", "index.d.ts", - "lib" + "lib", + "CHANGELOG.md" ], "dependencies": { "@node-oauth/formats": "1.0.0", diff --git a/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js new file mode 100644 index 00000000..5e71d4ab --- /dev/null +++ b/test/compliance/client-credential-workflow_test.js @@ -0,0 +1,142 @@ +/** + * 4.4. Client Credentials Grant + * + * The client can request an access token using only its client + * credentials (or other supported means of authentication) when the + * client is requesting access to the protected resources under its + * control, or those of another resource owner that have been previously + * arranged with the authorization server (the method of which is beyond + * the scope of this specification). + * + * The client credentials grant type MUST only be used by confidential + * clients. + * + * @see https://www.rfc-editor.org/rfc/rfc6749#section-4.4 + */ + +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); + +require('chai').should(); + +const db = new DB(); +// this user represents requests in the name of an external server +// TODO: we should discuss, if we can make user optional for client credential workflows +// as it's not desired to have an extra fake-user representing a server just to pass validation +const userDoc = { id: 'machine2-123456789', name: 'machine2' }; +db.saveUser(userDoc); + +const oAuth2Server = new OAuth2Server({ + model: { + ...createModel(db), + getUserFromClient: async function (_client) { + // in a machine2machine setup we might not have a dedicated "user" + // but we need to return a truthy response to + const client = db.findClient(_client.id, _client.secret); + return client && { ...userDoc }; + } + } +}); + +const clientDoc = db.saveClient({ + id: 'client-credential-test-client', + secret: 'client-credential-test-secret', + grants: ['client_credentials'] +}); + +const enabledScope = 'read write'; + +describe('ClientCredentials Workflow Compliance (4.4)', function () { + describe('Access Token Request (4.4.1)', function () { + /** + * 4.4.2. Access Token Request + * + * The client makes a request to the token endpoint by adding the + * following parameters using the "application/x-www-form-urlencoded" + * format per Appendix B with a character encoding of UTF-8 in the HTTP + * request entity-body: + * + * grant_type + * REQUIRED. Value MUST be set to "client_credentials". + * + * scope + * OPTIONAL. The scope of the access request as described by + * Section 3.3. + * + * The client MUST authenticate with the authorization server as + * described in Section 3.2.1. + */ + it('authenticates the client with valid credentials', async function () { + const response = new Response(); + const request = createRequest({ + body: { + grant_type: 'client_credentials', + scope: enabledScope + }, + headers: { + 'authorization': 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded' + }, + method: 'POST', + }); + + const token = await oAuth2Server.token(request, response); + + response.status.should.equal(200); + response.headers.should.deep.equal( { 'cache-control': 'no-store', pragma: 'no-cache' }); + response.body.token_type.should.equal('Bearer'); + response.body.access_token.should.equal(token.accessToken); + response.body.expires_in.should.be.a('number'); + response.body.scope.should.eql(['read', 'write']); + ('refresh_token' in response.body).should.equal(false); + + token.accessToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + ('refreshToken' in token).should.equal(false); + ('refreshTokenExpiresAt' in token).should.equal(false); + token.scope.should.eql(['read', 'write']); + + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(false); + }); + + /** + * 7. Accessing Protected Resources + * + * The client accesses protected resources by presenting the access + * token to the resource server. The resource server MUST validate the + * access token and ensure that it has not expired and that its scope + * covers the requested resource. The methods used by the resource + * server to validate the access token (as well as any error responses) + * are beyond the scope of this specification but generally involve an + * interaction or coordination between the resource server and the + * authorization server. + */ + it('enables an authenticated request using the access token', async function () { + const [accessToken] = [...db.accessTokens.entries()][0]; + const response = new Response(); + const request = createRequest({ + query: {}, + headers: { + 'authorization': `Bearer ${accessToken}` + }, + method: 'GET', + }); + + const token = await oAuth2Server.authenticate(request, response); + token.accessToken.should.equal(accessToken); + token.user.should.deep.equal(userDoc); + token.client.should.deep.equal(clientDoc); + token.scope.should.eql(['read', 'write']); + + response.status.should.equal(200); + // there should be no information in the response as it + // should only add information, if permission is denied + response.body.should.deep.equal({}); + response.headers.should.deep.equal({}); + }); + }); +}); diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index 7941d54f..c30e440d 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -101,13 +101,13 @@ describe('PasswordGrantType Compliance', function () { response.body.access_token.should.equal(token.accessToken); response.body.refresh_token.should.equal(token.refreshToken); response.body.expires_in.should.be.a('number'); - response.body.scope.should.equal(scope); + response.body.scope.should.eql(['read', 'write']); token.accessToken.should.be.a('string'); token.refreshToken.should.be.a('string'); token.accessTokenExpiresAt.should.be.a('date'); token.refreshTokenExpiresAt.should.be.a('date'); - token.scope.should.equal(scope); + token.scope.should.eql(['read', 'write']); db.accessTokens.has(token.accessToken).should.equal(true); db.refreshTokens.has(token.refreshToken).should.equal(true); @@ -134,7 +134,7 @@ describe('PasswordGrantType Compliance', function () { authenticationResponse, {}); - authenticated.scope.should.equal(scope); + authenticated.scope.should.eql(['read', 'write']); authenticated.user.should.be.an('object'); authenticated.client.should.be.an('object'); }); diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index b01fef3d..09427855 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -62,6 +62,7 @@ const DB = require('../helpers/db'); const createModel = require('../helpers/model'); const createRequest = require('../helpers/request'); const Response = require('../../lib/response'); +const should = require('chai').should(); require('chai').should(); @@ -123,13 +124,13 @@ describe('RefreshTokenGrantType Compliance', function () { refreshResponse.body.access_token.should.equal(token.accessToken); refreshResponse.body.refresh_token.should.equal(token.refreshToken); refreshResponse.body.expires_in.should.be.a('number'); - refreshResponse.body.scope.should.equal(scope); + refreshResponse.body.scope.should.eql(['read', 'write']); token.accessToken.should.be.a('string'); token.refreshToken.should.be.a('string'); token.accessTokenExpiresAt.should.be.a('date'); token.refreshTokenExpiresAt.should.be.a('date'); - token.scope.should.equal(scope); + token.scope.should.eql(['read', 'write']); db.accessTokens.has(token.accessToken).should.equal(true); db.refreshTokens.has(token.refreshToken).should.equal(true); @@ -147,27 +148,82 @@ describe('RefreshTokenGrantType Compliance', function () { }); }); - // TODO: test refresh token with different scopes - // https://github.com/node-oauth/node-oauth2-server/issues/104 + it('Should throw invalid_scope error', async function () { + const request = createLoginRequest(); + const response = new Response({}); - // it('Should throw invalid_scope error', async function () { - // const request = createLoginRequest(); - // const response = new Response({}); + const credentials = await auth.token(request, response, {}); - // const credentials = await auth.token(request, response, {}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - // const refreshRequest = createRefreshRequest(credentials.refreshToken); - // const refreshResponse = new Response({}); + refreshRequest.body.scope = 'invalid'; - // refreshRequest.scope = 'invalid'; + await auth.token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch(err => { + err.name.should.equal('invalid_scope'); + }); + }); + + it('Should throw error if requested scope is greater than original scope', async function () { + const request = createLoginRequest(); + const response = new Response({}); + + request.body.scope = 'read'; + + const credentials = await auth.token(request, response, {}); - // await auth.token(refreshRequest, refreshResponse, {}) - // .then(() => { - // throw Error('Should not reach this'); - // }) - // .catch(err => { - // err.name.should.equal('invalid_scope'); - // }); - // }); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); + + refreshRequest.scope = 'read write'; + + await auth.token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch(err => { + err.name.should.equal('invalid_scope'); + }); + }); + + it('Should throw error if a scope is requested without a previous scope', async function () { + const request = createLoginRequest(); + const response = new Response({}); + + delete request.body.scope; + + const credentials = await auth.token(request, response, {}); + + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); + + refreshRequest.scope = 'read write'; + + await auth.token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch(err => { + err.name.should.equal('invalid_scope'); + }); + }); + + it('Should create refresh token with smaller scope', async function () { + const request = createLoginRequest(); + const response = new Response({}); + + const credentials = await auth.token(request, response, {}); + + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); + + refreshRequest.body.scope = 'read'; + + const token = await auth.token(refreshRequest, refreshResponse, {}); + + refreshResponse.body.token_type.should.equal('Bearer'); + refreshResponse.body.access_token.should.equal(token.accessToken); + refreshResponse.body.refresh_token.should.equal(token.refreshToken); + refreshResponse.body.expires_in.should.be.a('number'); + refreshResponse.body.scope.should.eql(['read']); + }); }); }); diff --git a/test/helpers/model.js b/test/helpers/model.js index 7a1893b1..6566f0cd 100644 --- a/test/helpers/model.js +++ b/test/helpers/model.js @@ -71,11 +71,7 @@ function createModel (db) { } async function verifyScope (token, scope) { - if (typeof scope === 'string') { - return scopes.includes(scope); - } else { - return scope.every(s => scopes.includes(s)); - } + return scope.every(s => scopes.includes(s)); } return { diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index e874509f..d48c1ee0 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -7,6 +7,7 @@ const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); const Request = require('../../../lib/request'); +const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); const should = require('chai').should(); /** @@ -44,7 +45,7 @@ describe('AbstractGrantType integration', function() { }); it('should set the `model`', function() { - const model = {}; + const model = { async generateAccessToken () {} }; const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: model }); grantType.model.should.equal(model); @@ -58,70 +59,62 @@ describe('AbstractGrantType integration', function() { }); describe('generateAccessToken()', function() { - it('should return an access token', function() { + it('should return an access token', async function() { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateAccessToken() - .then(function(data) { - data.should.be.a.sha256(); - }) - .catch(should.fail); + const accessToken = await handler.generateAccessToken(); + accessToken.should.be.a.sha256(); }); - it('should support promises', function() { + it('should support promises', async function() { const model = { generateAccessToken: async function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal('long-hash-foo-bar'); }); - it('should support non-promises', function() { + it('should support non-promises', async function() { const model = { generateAccessToken: function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal('long-hash-foo-bar'); }); }); describe('generateRefreshToken()', function() { - it('should return a refresh token', function() { + it('should return a refresh token', async function() { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateRefreshToken() - .then(function(data) { - data.should.be.a.sha256(); - }) - .catch(should.fail); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.be.a.sha256(); }); - it('should support promises', function() { + it('should support promises', async function() { const model = { generateRefreshToken: async function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal('long-hash-foo-bar'); }); - it('should support non-promises', function() { + it('should support non-promises', async function() { const model = { generateRefreshToken: function() { - return {}; + return 'long-hash-foo-bar'; } }; const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal('long-hash-foo-bar'); }); }); @@ -151,7 +144,7 @@ describe('AbstractGrantType integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); + e.should.be.an.instanceOf(InvalidScopeError); e.message.should.equal('Invalid parameter: `scope`'); } }); @@ -167,7 +160,67 @@ describe('AbstractGrantType integration', function() { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - handler.getScope(request).should.equal('foo'); + handler.getScope(request).should.eql(['foo']); + }); + }); + + describe('validateScope()', function () { + it('accepts the scope, if the model does not implement it', async function () { + const scope = ['some,scope,this,that']; + const user = { id: 123 }; + const client = { id: 456 }; + const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); + const validated = await handler.validateScope(user, client, scope); + validated.should.eql(scope); + }); + + it('accepts the scope, if the model accepts it', async function () { + const scope = ['some,scope,this,that']; + const user = { id: 123 }; + const client = { id: 456 }; + + const model = { + async validateScope (_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.eql(scope); + + return scope; + } + }; + const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); + const validated = await handler.validateScope(user, client, scope); + validated.should.eql(scope); + }); + + it('throws if the model rejects the scope', async function () { + const scope = ['some,scope,this,that']; + const user = { id: 123 }; + const client = { id: 456 }; + const returnTypes = [undefined, null, false, 0, '']; + + for (const type of returnTypes) { + const model = { + async validateScope (_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.eql(scope); + + return type; + } + }; + const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); + + try { + await handler.validateScope(user, client, scope); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: Requested scope is invalid'); + } + } }); }); }); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index a4d69c40..d705f397 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -75,9 +75,9 @@ describe('AuthorizationCodeGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -88,31 +88,34 @@ describe('AuthorizationCodeGrantType integration', function() { e.message.should.equal('Missing parameter: `request`'); } }); - - it('should throw an error if `client` is invalid', function() { - const client = {}; + + it('should throw an error if `client` is invalid (not in code)', async function() { + const client = { id: 1234 }; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: function(code) { + code.should.equal(123456789); + return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); + const request = new Request({ body: { code: 123456789 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); + try { + await grantType.handle(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); + } }); it('should throw an error if `client` is missing', function() { - const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -128,18 +131,64 @@ describe('AuthorizationCodeGrantType integration', function() { it('should return a token', async function() { const client = { id: 'foobar' }; - const token = {}; + const scope = ['fooscope']; + const user = { name: 'foouser' }; + const codeDoc = { + authorizationCode: 12345, + expiresAt: new Date(new Date() * 2), + client, + user, + scope + }; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getAuthorizationCode: async function (code) { + code.should.equal('code-1234'); + + return codeDoc; + }, + revokeAuthorizationCode: async function (_codeDoc) { + _codeDoc.should.deep.equal(codeDoc); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return 'long-access-token-hash'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return 'long-refresh-token-hash'; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.accessToken.should.equal('long-access-token-hash'); + _token.refreshToken.should.equal('long-refresh-token-hash'); + _token.authorizationCode.should.equal(codeDoc.authorizationCode); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return _token; + }, }; - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - const data = await grantType.handle(request, client); - data.should.equal(token); + const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + const request = new Request({ body: { code: 'code-1234' }, headers: {}, method: {}, query: {} }); + + const token = await grantType.handle(request, client); + token.accessToken.should.equal('long-access-token-hash'); + token.refreshToken.should.equal('long-refresh-token-hash'); + token.authorizationCode.should.equal(codeDoc.authorizationCode); + token.accessTokenExpiresAt.should.be.instanceOf(Date); + token.refreshTokenExpiresAt.should.be.instanceOf(Date); }); it('should support promises', function() { @@ -173,9 +222,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the request body does not contain `code`', async function() { const client = {}; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -191,9 +240,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `code` is invalid', async function() { const client = {}; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 'ø倣‰' }, headers: {}, method: {}, query: {} }); @@ -210,9 +259,9 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authorizationCode` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() {}, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); @@ -226,136 +275,150 @@ describe('AuthorizationCodeGrantType integration', function() { } }); - it('should throw an error if `authorizationCode.client` is missing', function() { + it('should throw an error if `authorizationCode.client` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345 }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { return { authorizationCode: 12345 }; }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); + } }); - it('should throw an error if `authorizationCode.expiresAt` is missing', function() { + it('should throw an error if `authorizationCode.expiresAt` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { + return { authorizationCode: 12345, client: {}, user: {} }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `expiresAt` must be a Date instance'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `expiresAt` must be a Date instance'); + } }); - it('should throw an error if `authorizationCode.user` is missing', function() { + it('should throw an error if `authorizationCode.user` is missing', async function() { const client = {}; const model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { + return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); + } }); - it('should throw an error if the client id does not match', function() { + it('should throw an error if the client id does not match', async function() { const client = { id: 123 }; const model = { - getAuthorizationCode: function() { + getAuthorizationCode: async function() { return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code is invalid'); + } }); - it('should throw an error if the auth code is expired', function() { + it('should throw an error if the auth code is expired', async function() { const client = { id: 123 }; const date = new Date(new Date() / 2); const model = { - getAuthorizationCode: function() { + getAuthorizationCode: async function() { return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code has expired'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code has expired'); + } }); - it('should throw an error if the `redirectUri` is invalid', function() { + it('should throw an error if the `redirectUri` is invalid (format)', async function() { const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', user: {} }; const client = { id: 'foobar' }; const model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function() { return authorizationCode; }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); - }); + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); + } }); - it('should return an auth code', function() { - const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; + it('should return an auth code', async function() { + const authorizationCode = { + authorizationCode: 1234567, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), user: {} + }; const client = { id: 'foobar' }; const model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} + getAuthorizationCode: async function(_code) { + _code.should.equal(12345); + return authorizationCode; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getAuthorizationCode(request, client) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); + const code = await grantType.getAuthorizationCode(request, client); + code.should.deep.equal(authorizationCode); }); it('should support promises', function() { @@ -427,85 +490,113 @@ describe('AuthorizationCodeGrantType integration', function() { e.message.should.equal('Invalid request: `redirect_uri` is invalid'); } }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should revoke the auth code', function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + it('returns undefined and does not throw if `redirectUri` is valid', async function () { + const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; const model = { getAuthorizationCode: function() {}, revokeAuthorizationCode: function() { return true; }, saveToken: function() {} }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); + const request = new Request({ body: { code: 12345, redirect_uri: 'http://foo.bar' }, headers: {}, method: {}, query: {} }); + const value = grantType.validateRedirectUri(request, authorizationCode); + const isUndefined = value === undefined; + isUndefined.should.equal(true); }); + }); - it('should throw an error when the auth code is invalid', function() { + describe('revokeAuthorizationCode()', function() { + it('should revoke the auth code', async function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return false; }, - saveToken: function() {} + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function(_code) { + _code.should.equal(authorizationCode); + return true; + }, + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(function(e) { + const data = await grantType.revokeAuthorizationCode(authorizationCode); + data.should.deep.equal(authorizationCode); + }); + + it('should throw an error when the auth code is invalid', async function() { + const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + const returnTypes = [false, null, undefined, 0, '']; + + for (const type of returnTypes) { + const model = { + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function(_code) { + _code.should.equal(authorizationCode); + return type; + }, + saveToken: () => should.fail() + }; + const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + + try { + await grantType.revokeAuthorizationCode(authorizationCode); + should.fail(); + } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); e.message.should.equal('Invalid grant: authorization code is invalid'); - }); + } + } }); it('should support promises', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; const model = { - getAuthorizationCode: function() {}, + getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: async function() { return true; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; const model = { - getAuthorizationCode: function() {}, + getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); }); describe('saveToken()', function() { - it('should save the token', function() { - const token = {}; + it('should save the token', async function() { + const token = { foo: 'bar' }; const model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: function(_token, _client= 'fallback', _user= 'fallback') { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.eql(['foo']); + (_token.authorizationCode === undefined).should.equal(true); + _user.should.equal('fallback'); + _client.should.equal('fallback'); + return token; + }, + validateScope: function(_user= 'fallback', _client= 'fallback', _scope = ['fallback']) { + _user.should.equal('fallback'); + _client.should.equal('fallback'); + _scope.should.eql(['fallback']); + return ['foo']; + } }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.saveToken(); + data.should.equal(token); }); it('should support promises', function() { diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 83de9f9a..97d10055 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -90,28 +90,50 @@ describe('ClientCredentialsGrantType integration', function() { } }); - it('should return a token', function() { + it('should return a token', async function() { const token = {}; + const client = { foo: 'bar' }; + const user = { name: 'foo' }; + const scope = ['fooscope']; + const model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getUserFromClient: async function(_client) { + _client.should.deep.equal(client); + return { ...user }; + }, + saveToken: async function(_token, _client, _user) { + _client.should.deep.equal(client); + _user.should.deep.equal(user); + _token.accessToken.should.equal('long-access-token-hash'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.scope.should.eql(scope); + return token; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return 'long-access-token-hash'; + } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + const request = new Request({ body: { scope: scope.join(' ') }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, {}) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.handle(request, client); + data.should.equal(token); }); it('should support promises', function() { const token = {}; const model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } + getUserFromClient: async function() { return {}; }, + saveToken: async function() { return token; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -136,7 +158,7 @@ describe('ClientCredentialsGrantType integration', function() { it('should throw an error if `user` is missing', function() { const model = { getUserFromClient: function() {}, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -153,7 +175,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUserFromClient: function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -169,7 +191,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUserFromClient: async function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -181,7 +203,7 @@ describe('ClientCredentialsGrantType integration', function() { const user = { email: 'foo@bar.com' }; const model = { getUserFromClient: function() {return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -191,26 +213,22 @@ describe('ClientCredentialsGrantType integration', function() { }); describe('saveToken()', function() { - it('should save the token', function() { + it('should save the token', async function() { const token = {}; const model = { - getUserFromClient: function() {}, + getUserFromClient: () => should.fail(), saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + validateScope: function() { return ['foo']; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.saveToken(token); + data.should.equal(token); }); it('should support promises', function() { const token = {}; const model = { - getUserFromClient: function() {}, + getUserFromClient:() => should.fail(), saveToken: async function() { return token; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); @@ -221,7 +239,7 @@ describe('ClientCredentialsGrantType integration', function() { it('should support non-promises', function() { const token = {}; const model = { - getUserFromClient: function() {}, + getUserFromClient: () => should.fail(), saveToken: function() { return token; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index 04452ee0..ef9b2f16 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -45,7 +45,7 @@ describe('PasswordGrantType integration', function() { getUser: function() {} }; - new PasswordGrantType({ model: model }); + new PasswordGrantType({ model }); should.fail(); } catch (e) { @@ -58,10 +58,10 @@ describe('PasswordGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); try { await grantType.handle(); @@ -75,10 +75,10 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `client` is missing', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); try { await grantType.handle({}); @@ -90,32 +90,66 @@ describe('PasswordGrantType integration', function() { } }); - it('should return a token', function() { + it('should return a token', async function() { const client = { id: 'foobar' }; + const scope = ['baz']; const token = {}; + const user = { + id: 123456, + username: 'foo', + email: 'foo@example.com' + }; + const model = { - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } + getUser: async function(username, password) { + username.should.equal('foo'); + password.should.equal('bar'); + return user; + }, + validateScope: async function(_user, _client, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return 'long-access-token-hash'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return 'long-refresh-token-hash'; + }, + saveToken: async function(_token, _client, _user) { + _client.should.equal(client); + _user.should.equal(user); + _token.accessToken.should.equal('long-access-token-hash'); + _token.refreshToken.should.equal('long-refresh-token-hash'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.handle(request, client); + data.should.equal(token); }); it('should support promises', async function() { const client = { id: 'foobar' }; const token = {}; const model = { - getUser: function() { return {}; }, + getUser: async function() { return {}; }, saveToken: async function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); const result = await grantType.handle(request, client); @@ -129,7 +163,7 @@ describe('PasswordGrantType integration', function() { getUser: function() { return {}; }, saveToken: function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); const result = await grantType.handle(request, client); @@ -140,14 +174,15 @@ describe('PasswordGrantType integration', function() { describe('getUser()', function() { it('should throw an error if the request body does not contain `username`', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { - await grantType.getUser(request); + await grantType.getUser(request, client); should.fail(); } catch (e) { @@ -158,14 +193,15 @@ describe('PasswordGrantType integration', function() { it('should throw an error if the request body does not contain `password`', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); try { - await grantType.getUser(request); + await grantType.getUser(request, client); should.fail(); } catch (e) { @@ -176,14 +212,15 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `username` is invalid', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, query: {} }); try { - await grantType.getUser(request); + await grantType.getUser(request, client); should.fail(); } catch (e) { @@ -194,14 +231,15 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `password` is invalid', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, query: {} }); try { - await grantType.getUser(request); + await grantType.getUser(request, client); should.fail(); } catch (e) { @@ -210,87 +248,102 @@ describe('PasswordGrantType integration', function() { } }); - it('should throw an error if `user` is missing', function() { + it('should throw an error if `user` is missing', async function() { const model = { - getUser: function() {}, - saveToken: function() {} + getUser: async () => undefined, + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); + try { + await grantType.getUser(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + } }); - it('should return a user', function() { + it('should return a user', async function() { const user = { email: 'foo@bar.com' }; + const client = { id: 'foobar' }; const model = { - getUser: function() { return user; }, - saveToken: function() {} + getUser: function(username, password) { + username.should.equal('foo'); + password.should.equal('bar'); + return user; + }, + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); + const data = await grantType.getUser(request, client); + data.should.equal(user); }); it('should support promises', function() { const user = { email: 'foo@bar.com' }; + const client = { id: 'foobar' }; const model = { getUser: async function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - grantType.getUser(request).should.be.an.instanceOf(Promise); + grantType.getUser(request, client).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { const user = { email: 'foo@bar.com' }; + const client = { id: 'foobar' }; const model = { getUser: function() { return user; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - grantType.getUser(request).should.be.an.instanceOf(Promise); + grantType.getUser(request, client).should.be.an.instanceOf(Promise); }); }); describe('saveToken()', function() { - it('should save the token', function() { + it('should save the token', async function() { const token = {}; const model = { - getUser: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + getUser: () => should.fail(), + saveToken: async function(_token, _client = 'fallback', _user = 'fallback') { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.eql(['foo']); + _client.should.equal('fallback'); + _user.should.equal('fallback'); + return token; + }, + validateScope: async function(_scope = ['fallback']) { + _scope.should.eql(['fallback']); + return ['foo']; + } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.saveToken(); + data.should.equal(token); }); it('should support promises', function() { const token = {}; const model = { - getUser: function() {}, + getUser: () => should.fail(), saveToken: async function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); @@ -298,10 +351,10 @@ describe('PasswordGrantType integration', function() { it('should support non-promises', function() { const token = {}; const model = { - getUser: function() {}, + getUser: () => should.fail(), saveToken: function() { return token; } }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index fede3776..0619fefd 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -43,10 +43,10 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `revokeToken()`', function() { try { const model = { - getRefreshToken: function() {} + getRefreshToken: () => should.fail() }; - new RefreshTokenGrantType({ model: model }); + new RefreshTokenGrantType({ model }); should.fail(); } catch (e) { @@ -58,11 +58,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `saveToken()`', function() { try { const model = { - getRefreshToken: function() {}, - revokeToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail() }; - new RefreshTokenGrantType({ model: model }); + new RefreshTokenGrantType({ model }); should.fail(); } catch (e) { @@ -75,11 +75,11 @@ describe('RefreshTokenGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', async function() { const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); try { await grantType.handle(); @@ -93,11 +93,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `client` is missing', async function() { const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -110,22 +110,51 @@ describe('RefreshTokenGrantType integration', function() { } }); - it('should return a token', function() { + it('should return a token', async function() { const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const token = { + accessToken: 'foo', + client: { id: 123 }, + user: { name: 'foo' }, + scope: ['read', 'write'], + refreshTokenExpiresAt: new Date( new Date() * 2) + }; const model = { - getRefreshToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return token; } + getRefreshToken: async function(_refreshToken) { + _refreshToken.should.equal('foobar_refresh'); + return token; + }, + revokeToken: async function(_token) { + _token.should.deep.equal(token); + return true; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _scope.should.eql(['read', 'write']); + return 'new-access-token'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _scope.should.eql(['read', 'write']); + return 'new-refresh-token'; + }, + saveToken: async function(_token, _client, _user) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _token.accessToken.should.equal('new-access-token'); + _token.refreshToken.should.equal('new-refresh-token'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); + const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); + const data = await grantType.handle(request, client); + data.should.equal(token); }); it('should support promises', function() { @@ -135,7 +164,7 @@ describe('RefreshTokenGrantType integration', function() { revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -144,11 +173,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return { accessToken: 'foo', client: {}, user: {} }; } + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, + revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, + saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -159,11 +188,11 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the `refreshToken` parameter is missing from the request body', async function() { const client = {}; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -176,92 +205,100 @@ describe('RefreshTokenGrantType integration', function() { } }); - it('should throw an error if `refreshToken` is not found', function() { + it('should throw an error if `refreshToken` is not found', async function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { return; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function() {} , + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + } }); - it('should throw an error if `refreshToken.client` is missing', function() { + it('should throw an error if `refreshToken.client` is missing', async function() { const client = {}; const model = { - getRefreshToken: function() { return {}; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function() { return {}; }, + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); + } }); - it('should throw an error if `refreshToken.user` is missing', function() { + it('should throw an error if `refreshToken.user` is missing', async function() { const client = {}; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); + } }); - it('should throw an error if the client id does not match', function() { + it('should throw an error if the client id does not match', async function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token was issued to another client'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token was issued to another client'); + } }); it('should throw an error if `refresh_token` contains invalid characters', async function() { const client = {}; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { client: { id: 456 }, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 'ø倣‰' }, headers: {}, method: {}, query: {} }); try { @@ -274,83 +311,100 @@ describe('RefreshTokenGrantType integration', function() { } }); - it('should throw an error if `refresh_token` is missing', function() { + it('should throw an error if `refresh_token` is missing', async function() { const client = {}; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token was issued to another client'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token was issued to another client'); + } }); - it('should throw an error if `refresh_token` is expired', function() { + it('should throw an error if `refresh_token` is expired', async function() { const client = { id: 123 }; const date = new Date(new Date() / 2); const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token has expired'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token has expired'); + } }); - it('should throw an error if `refreshTokenExpiresAt` is not a date value', function() { + it('should throw an error if `refreshTokenExpiresAt` is not a date value', async function() { const client = { id: 123 }; const model = { - getRefreshToken: function() { + getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', user: {} }; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: () => should.fail(), + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); - }); + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); + } }); - it('should return a token', function() { + it('should return a token', async function() { const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const token = { accessToken: 'foo', client: { id: 123 }, user: { name: 'foobar' } }; const model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function(_refreshToken) { + _refreshToken.should.equal('foobar_refresh'); + return token; + }, + revokeToken: async function(_token) { + _token.should.deep.equal(token); + return true; + }, + saveToken: async function(_token, _client, _user) { + _user.should.deep.equal(token.user); + _client.should.deep.equal(client); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); + const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.getRefreshToken(request, client); + data.should.equal(token); }); it('should support promises', function() { @@ -358,10 +412,10 @@ describe('RefreshTokenGrantType integration', function() { const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; const model = { getRefreshToken: async function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} + revokeToken: async function() {}, + saveToken: async function() {} }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); @@ -371,11 +425,11 @@ describe('RefreshTokenGrantType integration', function() { const client = { id: 123 }; const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; const model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: async function() { return token; }, + revokeToken: async function() {}, + saveToken: async function() {} }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); @@ -383,46 +437,47 @@ describe('RefreshTokenGrantType integration', function() { }); describe('revokeToken()', function() { - it('should throw an error if the `token` is invalid', function() { + it('should throw an error if the `token` is invalid', async function() { const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: async () => {}, + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - grantType.revokeToken({}) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); + + try { + await grantType.revokeToken({}); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid or could not be revoked'); + } }); - it('should revoke the token', function() { + it('should revoke the token', async function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; const model = { - getRefreshToken: function() {}, - revokeToken: function() { return token; }, - saveToken: function() {} + getRefreshToken: () => should.fail(), + revokeToken: async function(_token) { + _token.should.deep.equal(token); + return token; + }, + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - return grantType.revokeToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const data = await grantType.revokeToken(token); + data.should.equal(token); }); it('should support promises', function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; const model = { - getRefreshToken: function() {}, + getRefreshToken: () => should.fail(), revokeToken: async function() { return token; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); }); @@ -430,41 +485,53 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; const model = { - getRefreshToken: function() {}, + getRefreshToken: () => should.fail(), revokeToken: function() { return token; }, - saveToken: function() {} + saveToken: () => should.fail() }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); }); }); describe('saveToken()', function() { - it('should save the token', function() { - const token = {}; + it('should save the token', async function() { + const user = { name: 'foo' }; + const client = { id: 123465 }; + const scope = ['foo', 'bar']; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return token; } + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: async function(_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.scope.should.deep.eql(scope); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return { ..._token }; + } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); + + const data = await grantType.saveToken(user, client, scope); + data.accessToken.should.be.a.sha256(); + data.refreshToken.should.be.a.sha256(); + data.accessTokenExpiresAt.should.be.instanceOf(Date); + data.refreshTokenExpiresAt.should.be.instanceOf(Date); + data.scope.should.deep.equal(scope); }); it('should support promises', function() { const token = {}; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), saveToken: async function() { return token; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); @@ -472,11 +539,11 @@ describe('RefreshTokenGrantType integration', function() { it('should support non-promises', function() { const token = {}; const model = { - getRefreshToken: function() {}, - revokeToken: function() {}, + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), saveToken: function() { return token; } }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); }); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index c069ed9f..52355550 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -46,7 +46,7 @@ describe('AuthenticateHandler integration', function() { it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', function() { try { - new AuthenticateHandler({ model: { getAccessToken: function() {} }, scope: 'foobar' }); + new AuthenticateHandler({ model: { getAccessToken: function() {} }, scope: ['foobar'] }); should.fail(); } catch (e) { @@ -57,7 +57,7 @@ describe('AuthenticateHandler integration', function() { it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', function() { try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, model: { getAccessToken: function() {} }, scope: 'foobar' }); + new AuthenticateHandler({ addAcceptedScopesHeader: true, model: { getAccessToken: function() {} }, scope: ['foobar'] }); should.fail(); } catch (e) { @@ -68,7 +68,7 @@ describe('AuthenticateHandler integration', function() { it('should throw an error if `scope` was given and the model does not implement `verifyScope()`', function() { try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: { getAccessToken: function() {} }, scope: 'foobar' }); + new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: { getAccessToken: function() {} }, scope: ['foobar'] }); should.fail(); } catch (e) { @@ -93,24 +93,46 @@ describe('AuthenticateHandler integration', function() { addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foobar' + scope: ['foobar'] }); - grantType.scope.should.equal('foobar'); + grantType.scope.should.eql(['foobar']); }); }); describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + it('should throw an error if `request` is missing or not a Request instance', async function() { + class Request {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Request()]; + for (const request of values) { + const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + + try { + await handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: `request` must be an instance of Request'); + } + } + }); - try { - await handler.handle(); + it('should throw an error if `response` is missing or not a Response instance', async function() { + class Response {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Response()]; + const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); + for (const response of values) { + const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + try { + await handler.handle(request, response); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: `response` must be an instance of Response'); + } } }); @@ -232,7 +254,7 @@ describe('AuthenticateHandler integration', function() { return true; } }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, @@ -500,9 +522,9 @@ describe('AuthenticateHandler integration', function() { return false; } }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - return handler.verifyScope('foo') + return handler.verifyScope(['foo']) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InsufficientScopeError); @@ -517,9 +539,9 @@ describe('AuthenticateHandler integration', function() { return true; } }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - handler.verifyScope('foo').should.be.an.instanceOf(Promise); + handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { @@ -529,9 +551,9 @@ describe('AuthenticateHandler integration', function() { return true; } }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - handler.verifyScope('foo').should.be.an.instanceOf(Promise); + handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); }); }); @@ -544,7 +566,7 @@ describe('AuthenticateHandler integration', function() { const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model }); const response = new Response({ body: {}, headers: {} }); - handler.updateResponse(response, { scope: 'foo biz' }); + handler.updateResponse(response, { scope: ['foo', 'biz'] }); response.headers.should.not.have.property('x-accepted-oauth-scopes'); }); @@ -554,10 +576,10 @@ describe('AuthenticateHandler integration', function() { getAccessToken: function() {}, verifyScope: function() {} }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: 'foo bar' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: ['foo', 'bar'] }); const response = new Response({ body: {}, headers: {} }); - handler.updateResponse(response, { scope: 'foo biz' }); + handler.updateResponse(response, { scope: ['foo', 'biz'] }); response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); }); @@ -570,7 +592,7 @@ describe('AuthenticateHandler integration', function() { const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model }); const response = new Response({ body: {}, headers: {} }); - handler.updateResponse(response, { scope: 'foo biz' }); + handler.updateResponse(response, { scope: ['foo', 'biz'] }); response.headers.should.not.have.property('x-oauth-scopes'); }); @@ -580,10 +602,10 @@ describe('AuthenticateHandler integration', function() { getAccessToken: function() {}, verifyScope: function() {} }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: 'foo bar' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: ['foo', 'bar'] }); const response = new Response({ body: {}, headers: {} }); - handler.updateResponse(response, { scope: 'foo biz' }); + handler.updateResponse(response, { scope: ['foo', 'biz'] }); response.get('X-OAuth-Scopes').should.equal('foo biz'); }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 5da1b393..8bc3ae09 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -20,6 +20,15 @@ const UnauthorizedClientError = require('../../../lib/errors/unauthorized-client const should = require('chai').should(); const url = require('url'); +const createModel = (model = {}) => { + return { + getAccessToken: () => should.fail(), + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail(), + ...model + }; +}; + /** * Test `AuthorizeHandler` integration. */ @@ -29,7 +38,6 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if `options.authorizationCodeLifetime` is missing', function() { try { new AuthorizeHandler(); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -40,7 +48,6 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if `options.model` is missing', function() { try { new AuthorizeHandler({ authorizationCodeLifetime: 120 }); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -51,7 +58,6 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `getClient()`', function() { try { new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -61,8 +67,7 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: function() {} } }); - + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: () => should.fail() } }); should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -72,12 +77,12 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `getAccessToken()`', function() { const model = { - getClient: function() {}, - saveAuthorizationCode: function() {} + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail() }; try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); should.fail(); } catch (e) { @@ -87,51 +92,58 @@ describe('AuthorizeHandler integration', function() { }); it('should set the `authorizationCodeLifetime`', function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.authorizationCodeLifetime.should.equal(120); }); - it('should set the `authenticateHandler`', function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + it('should throw if the custom `authenticateHandler` does not implement a `handle` method', function () { + const model = createModel(); + const authenticateHandler = {}; // misses handle() method + + try { + new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, model }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: authenticateHandler does not implement `handle()`'); + } + }); + it('should set the default `authenticateHandler`, if no custom one is passed', function() { + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); }); - it('should set the `model`', function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + it('should set the custom `authenticateHandler`, if valid', function () { + const model = createModel(); + + class CustomAuthenticateHandler { + async handle () {} + } + const authenticateHandler = new CustomAuthenticateHandler(); + const handler = new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, model }); + handler.authenticateHandler.should.be.an.instanceOf(CustomAuthenticateHandler); + handler.authenticateHandler.should.not.be.an.instanceOf(AuthenticateHandler); + }); + + it('should set the `model`', function() { + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.model.should.equal(model); }); }); describe('handle()', function() { it('should throw an error if `request` is missing', async function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); try { await handler.handle(); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -140,17 +152,12 @@ describe('AuthorizeHandler integration', function() { }); it('should throw an error if `response` is missing', async function() { - const model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const model = createModel(); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { await handler.handle(request); - should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -158,28 +165,35 @@ describe('AuthorizeHandler integration', function() { } }); - it('should redirect to an error response if user denied access', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if user denied access', async function() { + const client = { + id: 'client-12345', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; + const model = createModel({ + getAccessToken: async function(_token) { + _token.should.equal('foobarbazmootoken'); return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() {} - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + getClient: async function(clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + } + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { - client_id: 12345, + client_id: client.id, response_type: 'code' }, method: {}, headers: { - 'Authorization': 'Bearer foo' + 'Authorization': 'Bearer foobarbazmootoken' }, query: { state: 'foobar', @@ -188,29 +202,39 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Access denied: user denied access to application'); + response + .get('location') + .should + .equal('http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar'); + } }); - it('should redirect to an error response if a non-oauth error is thrown', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if a non-oauth error is thrown', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; + getClient: async function() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; }, - saveAuthorizationCode: function() { - throw new Error('Unhandled exception'); + saveAuthorizationCode: async function() { + throw new CustomError('Unhandled exception'); } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + class CustomError extends Error {} + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -226,29 +250,35 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); // non-oauth-errors are converted to ServerError + e.message.should.equal('Unhandled exception'); + response + .get('location') + .should + .equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); + } }); - it('should redirect to an error response if an oauth error is thrown', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if an oauth error is thrown', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { + saveAuthorizationCode: async function() { throw new AccessDeniedError('Cannot request this auth code'); } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -264,69 +294,87 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Cannot request this auth code'); + response + .get('location') + .should + .equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); + } }); - it('should redirect to a successful response with `code` and `state` if successful', function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = { - getAccessToken: function() { + it('should redirect to a successful response with `code` and `state` if successful', async function() { + const client = { + id: 'client-12343434', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; + const model = createModel({ + getAccessToken: async function(_token) { + _token.should.equal('foobarbaztokenmoo'); return { - client: client, + client, user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return client; + getClient: async function(clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + saveAuthorizationCode: async function() { + return { + authorizationCode: 'fooobar-long-authzcode-?', + client + }; } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { - client_id: 12345, + client_id: client.id, response_type: 'code' }, headers: { - 'Authorization': 'Bearer foo' + 'Authorization': 'Bearer foobarbaztokenmoo' }, method: {}, query: { - state: 'foobar' + state: 'foobarbazstatemoo' } }); const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - response.get('location').should.equal('http://example.com/cb?code=12345&state=foobar'); - }) - .catch(should.fail); - }); - - it('should redirect to an error response if `scope` is invalid', function() { - const model = { - getAccessToken: function() { + const data = await handler.handle(request, response); + data.authorizationCode.should.equal('fooobar-long-authzcode-?'); + data.client.should.deep.equal(client); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); + }); + + it('should redirect to an error response if `scope` is invalid', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { + saveAuthorizationCode: async function() { return {}; } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -343,14 +391,18 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + response.status.should.equal(302); + response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar'); + } }); - it('should redirect to a successful response if `model.validateScope` is not defined', function() { + it('should redirect to a successful response if `model.validateScope` is not defined', async function() { const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { getAccessToken: function() { @@ -364,10 +416,10 @@ describe('AuthorizeHandler integration', function() { return client; }, saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + return { authorizationCode: 'fooobar-long-authzcode-?', client }; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -379,42 +431,44 @@ describe('AuthorizeHandler integration', function() { method: {}, query: { scope: 'read', - state: 'foobar' + state: 'foobarbazstatemoo' } }); const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.eql({ - authorizationCode: 12345, - client: client - }); - }) - .catch(should.fail); + const data = await handler.handle(request, response); + data.should.deep.equal({ + authorizationCode: 'fooobar-long-authzcode-?', + client: client + }); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); }); - it('should redirect to an error response if `scope` is insufficient', function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; + it('should redirect to an error response if `scope` is insufficient (validateScope)', async function() { + const client = { id: 12345, grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { - getAccessToken: function() { + getAccessToken: async function() { return { client: client, - user: {}, + user: { name: 'foouser' }, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return client; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + saveAuthorizationCode: async function() { + return { authorizationCode: 12345, client }; }, - validateScope: function() { + validateScope: async function(_user, _client, _scope) { + _scope.should.eql(['read']); return false; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -431,29 +485,36 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch(e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: Requested scope is invalid'); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar'); + } }); - it('should redirect to an error response if `state` is missing', function() { - const model = { - getAccessToken: function() { + it('should redirect to an error response if `state` is missing', async function() { + const model = createModel({ + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { + saveAuthorizationCode: async function() { throw new AccessDeniedError('Cannot request this auth code'); } - }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -467,29 +528,34 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); + } }); - it('should redirect to an error response if `response_type` is invalid', function() { + it('should redirect to an error response if `response_type` is invalid', async function() { const model = { - getAccessToken: function() { + getAccessToken: async function() { return { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: {} }; - } + saveAuthorizationCode: () => should.fail() // should fail before call }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, @@ -505,33 +571,43 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedResponseTypeError); + e.message.should.equal('Unsupported response type: `response_type` is not supported'); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); + } }); - it('should fail on invalid `response_type` before calling model.saveAuthorizationCode()', function() { + it('should return the `code` if successful', async function() { + const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { - getAccessToken: function() { + getAccessToken: async function() { return { + client: client, user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; + getClient: async function() { + return client; }, - saveAuthorizationCode: function() { - throw new Error('must not be reached'); + generateAuthorizationCode: async () => 'some-code', + saveAuthorizationCode: async function(code) { + return { authorizationCode: code.authorizationCode, client: client }; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, - response_type: 'test' + response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' @@ -543,31 +619,119 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: 'some-code', + client: client + }); }); - it('should return the `code` if successful', function() { + it('should return the `code` if successful (full model implementation)', async function () { + const user = { name: 'fooUser' }; + const state = 'fooobarstatebaz'; + const scope = ['read']; + const client = { + id: 'client-1322132131', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'] + }; + const authorizationCode = 'long-authz-code'; + const accessTokenDoc = { + accessToken: 'some-access-token-code', + client, + user, + scope, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) + }; + const model = { + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + getAccessToken: async function (_token) { + _token.should.equal(accessTokenDoc.accessToken); + return { ...accessTokenDoc }; + }, + verifyScope: async function (_tokenDoc, _scope) { + _tokenDoc.should.equal(accessTokenDoc); + _scope.should.eql(accessTokenDoc.scope); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return _scope; + }, + generateAuthorizationCode: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return authorizationCode; + }, + saveAuthorizationCode: async function (code, _client, _user) { + code.authorizationCode.should.equal(authorizationCode); + code.expiresAt.should.be.instanceOf(Date); + _user.should.deep.equal(user); + _client.should.deep.equal(client); + return { ...code, client, user }; + } + }; + + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + const request = new Request({ + body: { + client_id: client.id, + response_type: 'code' + }, + headers: { + 'Authorization': `Bearer ${accessTokenDoc.accessToken}` + }, + method: {}, + query: { state, scope: scope.join(' ') } + }); + + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.scope.should.eql(scope); + data.client.should.deep.equal(client); + data.user.should.deep.equal(user); + data.expiresAt.should.be.instanceOf(Date); + data.redirectUri.should.equal(client.redirectUris[0]); + response.status.should.equal(302); + response + .get('location') + .should + .equal('http://example.com/cb?code=long-authz-code&state=fooobarstatebaz'); + }); + + it('should support a custom `authenticateHandler`', async function () { + const user = { name: 'user1' }; + const authenticateHandler = { + handle: async function () { + // all good + return { ...user }; + } + }; const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; const model = { - getAccessToken: function() { + getAccessToken: async function() { return { client: client, user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, - getClient: function() { + getClient: async function() { return client; }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; + generateAuthorizationCode: async () => 'some-code', + saveAuthorizationCode: async function(code) { + return { authorizationCode: code.authorizationCode, client: client }; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model, authenticateHandler }); const request = new Request({ body: { client_id: 12345, @@ -583,14 +747,11 @@ describe('AuthorizeHandler integration', function() { }); const response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response) - .then(function(data) { - data.should.eql({ - authorizationCode: 12345, - client: client - }); - }) - .catch(should.fail); + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: 'some-code', + client: client + }); }); }); @@ -601,7 +762,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); return handler.generateAuthorizationCode() .then(function(data) { @@ -619,7 +780,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); @@ -633,7 +794,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); @@ -646,7 +807,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); }); @@ -660,7 +821,7 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.validateRedirectUri('http://example.com/a', { redirectUris: ['http://example.com/a'] }).should.be.an.instanceOf(Promise); }); @@ -675,7 +836,7 @@ describe('AuthorizeHandler integration', function() { } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise); }); @@ -690,7 +851,7 @@ describe('AuthorizeHandler integration', function() { } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise); }); @@ -703,7 +864,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); try { @@ -722,7 +883,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 'ø倣‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); try { @@ -741,7 +902,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); try { @@ -760,7 +921,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -779,7 +940,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -798,7 +959,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -815,7 +976,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: ['authorization_code'] }; }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -834,7 +995,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -853,7 +1014,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345 }, headers: {}, @@ -872,7 +1033,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { client_id: 12345 }, headers: {}, @@ -893,7 +1054,7 @@ describe('AuthorizeHandler integration', function() { }, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: { client_id: 12345 } }); return handler.getClient(request) @@ -912,7 +1073,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); try { @@ -932,10 +1093,10 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - handler.getScope(request).should.equal('foo'); + handler.getScope(request).should.eql(['foo']); }); }); @@ -946,10 +1107,10 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { scope: 'foo' } }); - handler.getScope(request).should.equal('foo'); + handler.getScope(request).should.eql(['foo']); }); }); }); @@ -961,7 +1122,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -980,7 +1141,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const state = handler.getState(request); should.equal(state, undefined); @@ -992,7 +1153,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'ø倣‰' } }); try { @@ -1012,7 +1173,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); handler.getState(request).should.equal('foobar'); @@ -1026,7 +1187,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); handler.getState(request).should.equal('foobar'); @@ -1041,7 +1202,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const response = new Response(); @@ -1065,7 +1226,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -1087,7 +1248,7 @@ describe('AuthorizeHandler integration', function() { return authorizationCode; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); return handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz') .then(function(data) { @@ -1104,7 +1265,7 @@ describe('AuthorizeHandler integration', function() { return {}; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); @@ -1117,7 +1278,7 @@ describe('AuthorizeHandler integration', function() { return {}; } }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); @@ -1130,7 +1291,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -1149,7 +1310,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); try { @@ -1169,7 +1330,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); const ResponseType = handler.getResponseType(request); @@ -1184,7 +1345,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); const ResponseType = handler.getResponseType(request); @@ -1200,7 +1361,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const responseType = new CodeResponseType(12345); const redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); @@ -1216,7 +1377,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); @@ -1229,7 +1390,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=Bad%20Request'); @@ -1243,7 +1404,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const response = new Response({ body: {}, headers: {} }); const uri = url.parse('http://example.com/cb'); @@ -1260,7 +1421,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge_method: 'S256'}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1273,7 +1434,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge_method: 'foo'}, headers: {}, method: {}, query: {} }); try { @@ -1293,7 +1454,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1308,7 +1469,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {code_challenge: 'challenge'}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallenge(request); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 4477c7b8..1c2db3b4 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -298,12 +298,12 @@ describe('TokenHandler integration', function() { }); it('should return a bearer token if successful', function() { - const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {} }; + const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['foobar'], user: {} }; const model = { getClient: function() { return { grants: ['password'] }; }, getUser: function() { return {}; }, saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } + validateScope: function() { return ['baz']; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ @@ -329,12 +329,12 @@ describe('TokenHandler integration', function() { }); it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', function() { - const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {}, foo: 'bar' }; + const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['foobar'], user: {}, foo: 'bar' }; const model = { getClient: function() { return { grants: ['password'] }; }, getUser: function() { return {}; }, saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } + validateScope: function() { return ['baz']; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ @@ -364,12 +364,12 @@ describe('TokenHandler integration', function() { }); it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', function() { - const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {}, foo: 'bar' }; + const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['foobar'], user: {}, foo: 'bar' }; const model = { getClient: function() { return { grants: ['password'] }; }, getUser: function() { return {}; }, saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } + validateScope: function() { return ['baz']; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, allowExtendedTokenAttributes: true }); const request = new Request({ @@ -795,7 +795,7 @@ describe('TokenHandler integration', function() { getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, getClient: function() {}, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, + validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -834,7 +834,7 @@ describe('TokenHandler integration', function() { getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, + validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -872,7 +872,7 @@ describe('TokenHandler integration', function() { getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, + validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -910,7 +910,7 @@ describe('TokenHandler integration', function() { getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, + validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -949,7 +949,7 @@ describe('TokenHandler integration', function() { getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, + validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -984,7 +984,7 @@ describe('TokenHandler integration', function() { getAuthorizationCode: function() { return authorizationCode; }, getClient: function() {}, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, + validateScope: function() { return ['foo']; }, revokeAuthorizationCode: function() { return authorizationCode; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -1016,7 +1016,7 @@ describe('TokenHandler integration', function() { getClient: function() {}, getUserFromClient: function() { return {}; }, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + validateScope: function() { return ['foo']; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ @@ -1045,7 +1045,7 @@ describe('TokenHandler integration', function() { getClient: function() {}, getUser: function() { return {}; }, saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } + validateScope: function() { return ['baz']; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); const request = new Request({ @@ -1107,7 +1107,7 @@ describe('TokenHandler integration', function() { getClient: function() {}, getUser: function() { return {}; }, saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + validateScope: function() { return ['foo']; } }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, extendedGrantTypes: { 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType } }); const request = new Request({ body: { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); @@ -1176,8 +1176,8 @@ describe('TokenHandler integration', function() { saveToken: function() {} }; const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); - tokenType.should.deep.include({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: 'foobar' }); + const tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: ['foobar'] }); + tokenType.should.deep.include({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: ['foobar'] }); }); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index cb717c76..7732bdc2 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -17,14 +17,16 @@ const should = require('chai').should(); describe('Server integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { - try { - new Server({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } + [null, undefined, {}].forEach(options => { + try { + new Server(options); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); }); it('should set the `model`', function() { @@ -142,7 +144,7 @@ describe('Server integration', function() { saveToken: function() { return { accessToken: 1234, client: {}, user: {} }; }, - validateScope: function() { return 'foo'; } + validateScope: function() { return ['foo']; } }; const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass', scope: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); diff --git a/test/unit/errors/oauth-error_test.js b/test/unit/errors/oauth-error_test.js index bad86f65..6d68a299 100644 --- a/test/unit/errors/oauth-error_test.js +++ b/test/unit/errors/oauth-error_test.js @@ -16,7 +16,7 @@ describe('OAuthError', function() { describe('constructor()', function() { it('should get `captureStackTrace`', function() { - const errorFn = function () { throw new OAuthError('test', {name: 'test_error'}); }; + const errorFn = function () { throw new OAuthError('test', {name: 'test_error', foo: 'bar'}); }; try { errorFn(); @@ -25,6 +25,8 @@ describe('OAuthError', function() { } catch (e) { e.should.be.an.instanceOf(OAuthError); + e.name.should.equal('test_error'); + e.foo.should.equal('bar'); e.message.should.equal('test'); e.code.should.equal(500); e.stack.should.not.be.null; @@ -34,4 +36,23 @@ describe('OAuthError', function() { } }); }); + it('supports undefined properties', function () { + const errorFn = function () { throw new OAuthError('test'); }; + + try { + errorFn(); + + should.fail(); + } catch (e) { + + e.should.be.an.instanceOf(OAuthError); + e.name.should.equal('Error'); + e.message.should.equal('test'); + e.code.should.equal(500); + e.stack.should.not.be.null; + e.stack.should.not.be.undefined; + e.stack.should.include('oauth-error_test.js'); + e.stack.should.include('40'); //error lineNUmber + } + }); }); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index c3502bee..3ffe46ad 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -72,17 +72,17 @@ describe('AuthorizationCodeGrantType', function() { }; const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - sinon.stub(handler, 'validateScope').returns('foobiz'); + sinon.stub(handler, 'validateScope').returns(['foobiz']); sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foo')); sinon.stub(handler, 'generateRefreshToken').returns(Promise.resolve('bar')); sinon.stub(handler, 'getAccessTokenExpiresAt').returns(Promise.resolve('biz')); sinon.stub(handler, 'getRefreshTokenExpiresAt').returns(Promise.resolve('baz')); - return handler.saveToken(user, client, 'foobar', 'foobiz') + return handler.saveToken(user, client, 'foobar', ['foobiz']) .then(function() { model.saveToken.callCount.should.equal(1); model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', authorizationCode: 'foobar', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobiz' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', authorizationCode: 'foobar', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobiz'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); model.saveToken.firstCall.thisValue.should.equal(model); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index 3997823b..5e012b43 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -43,15 +43,15 @@ describe('ClientCredentialsGrantType', function() { }; const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - sinon.stub(handler, 'validateScope').returns('foobar'); + sinon.stub(handler, 'validateScope').returns(['foobar']); sinon.stub(handler, 'generateAccessToken').returns('foo'); sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - return handler.saveToken(user, client, 'foobar') + return handler.saveToken(user, client, ['foobar']) .then(function() { model.saveToken.callCount.should.equal(1); model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); model.saveToken.firstCall.thisValue.should.equal(model); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js index ceb2ad9d..63f43933 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -20,13 +20,14 @@ describe('PasswordGrantType', function() { getUser: sinon.stub().returns(true), saveToken: function() {} }; + const client = { id: 'foobar' }; const handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return handler.getUser(request) + return handler.getUser(request, client) .then(function() { model.getUser.callCount.should.equal(1); - model.getUser.firstCall.args.should.have.length(2); + model.getUser.firstCall.args.should.have.length(3); model.getUser.firstCall.args[0].should.equal('foo'); model.getUser.firstCall.args[1].should.equal('bar'); model.getUser.firstCall.thisValue.should.equal(model); @@ -45,17 +46,17 @@ describe('PasswordGrantType', function() { }; const handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); - sinon.stub(handler, 'validateScope').returns('foobar'); + sinon.stub(handler, 'validateScope').returns(['foobar']); sinon.stub(handler, 'generateAccessToken').returns('foo'); sinon.stub(handler, 'generateRefreshToken').returns('bar'); sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler.saveToken(user, client, 'foobar') + return handler.saveToken(user, client, ['foobar']) .then(function() { model.saveToken.callCount.should.equal(1); model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); model.saveToken.firstCall.thisValue.should.equal(model); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index c91a37ed..8d2faee6 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -131,11 +131,11 @@ describe('RefreshTokenGrantType', function() { sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler.saveToken(user, client, 'foobar') + return handler.saveToken(user, client, ['foobar']) .then(function() { model.saveToken.callCount.should.equal(1); model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); model.saveToken.firstCall.thisValue.should.equal(model); @@ -158,11 +158,11 @@ describe('RefreshTokenGrantType', function() { sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler.saveToken(user, client, 'foobar') + return handler.saveToken(user, client, ['foobar']) .then(function() { model.saveToken.callCount.should.equal(1); model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); model.saveToken.firstCall.thisValue.should.equal(model); @@ -185,11 +185,11 @@ describe('RefreshTokenGrantType', function() { sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler.saveToken(user, client, 'foobar') + return handler.saveToken(user, client, ['foobar']) .then(function() { model.saveToken.callCount.should.equal(1); model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); model.saveToken.firstCall.thisValue.should.equal(model); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index ff0a924d..c8433057 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -166,13 +166,13 @@ describe('AuthenticateHandler', function() { getAccessToken: function() {}, verifyScope: sinon.stub().returns(true) }; - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'bar' }); + const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['bar'] }); - return handler.verifyScope('foo') + return handler.verifyScope(['foo']) .then(function() { model.verifyScope.callCount.should.equal(1); model.verifyScope.firstCall.args.should.have.length(2); - model.verifyScope.firstCall.args[0].should.equal('foo', 'bar'); + model.verifyScope.firstCall.args[0].should.eql(['foo'], ['bar']); model.verifyScope.firstCall.thisValue.should.equal(model); }) .catch(should.fail); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 91ab651e..078f82f8 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -86,11 +86,11 @@ describe('AuthorizeHandler', function() { }; const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz', 'boz') + return handler.saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz') .then(function() { model.saveAuthorizationCode.callCount.should.equal(1); model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: 'qux' }); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: ['qux'] }); model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); @@ -106,11 +106,11 @@ describe('AuthorizeHandler', function() { }; const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod') + return handler.saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod') .then(function() { model.saveAuthorizationCode.callCount.should.equal(1); model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: 'qux', codeChallenge: 'codeChallenge', codeChallengeMethod: 'codeChallengeMethod' }); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: ['qux'], codeChallenge: 'codeChallenge', codeChallengeMethod: 'codeChallengeMethod' }); model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 19c00bf8..3f2688df 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -1,4 +1,5 @@ const TokenModel = require('../../../lib/models/token-model'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); const should = require('chai').should(); /** * Test `Server`. @@ -6,6 +7,101 @@ const should = require('chai').should(); describe('Model', function() { describe('constructor()', function() { + it('throws, if data is empty', function () { + try { + new TokenModel(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + it('throws, if `accessToken` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + client: 'bar', + user: 'tar', + accessTokenExpiresAt: atExpiresAt + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + it('throws, if `client` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + user: 'tar', + accessTokenExpiresAt: atExpiresAt + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + it('throws, if `user` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + client: 'bar', + accessTokenExpiresAt: atExpiresAt + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `user`'); + } + }); + it('throws, if `accessTokenExpiresAt` is not a Date', function () { + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + accessTokenExpiresAt: '11/10/2023' + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `accessTokenExpiresAt`'); + } + }); + it('throws, if `refreshTokenExpiresAt` is not a Date', function () { + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + refreshTokenExpiresAt: '11/10/2023' + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `refreshTokenExpiresAt`'); + } + }); it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', function() { const atExpiresAt = new Date(); atExpiresAt.setHours(new Date().getHours() + 1); diff --git a/test/unit/request_test.js b/test/unit/request_test.js index fe3c136e..ee5a2761 100644 --- a/test/unit/request_test.js +++ b/test/unit/request_test.js @@ -5,6 +5,7 @@ */ const Request = require('../../lib/request'); +const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); const should = require('chai').should(); /** @@ -27,6 +28,24 @@ function generateBaseRequest() { } describe('Request', function() { + it('should throw on missing args', function () { + const args = [ + [undefined, InvalidArgumentError, 'Missing parameter: `headers`'], + [null, TypeError, 'Cannot destructure property \'headers\''], + [{}, InvalidArgumentError, 'Missing parameter: `headers`'], + [{ headers: { }}, InvalidArgumentError, 'Missing parameter: `method`'], + [{ headers: {}, method: 'GET' }, InvalidArgumentError, 'Missing parameter: `query`'], + ]; + + args.forEach(([value, error, message]) => { + try { + new Request(value); + } catch (e) { + e.should.be.instanceOf(error); + e.message.should.include(message); + } + }); + }); it('should instantiate with a basic request', function() { const originalRequest = generateBaseRequest(); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index df685213..fd7bd391 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -30,24 +30,6 @@ describe('Server', function() { AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); AuthenticateHandler.prototype.handle.restore(); }); - - it('should map string passed as `options` to `options.scope`', function() { - const model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - const server = new Server({ model: model }); - - sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); - - server.authenticate('foo', 'bar', 'test'); - - AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthenticateHandler.prototype.handle.firstCall.args[1].should.equal('bar'); - AuthenticateHandler.prototype.handle.firstCall.thisValue.should.have.property('scope', 'test'); - AuthenticateHandler.prototype.handle.restore(); - }); }); describe('authorize()', function() { diff --git a/test/unit/utils/crypto-util_test.js b/test/unit/utils/crypto-util_test.js new file mode 100644 index 00000000..7c3057e0 --- /dev/null +++ b/test/unit/utils/crypto-util_test.js @@ -0,0 +1,18 @@ +const cryptoUtil = require('../../../lib/utils/crypto-util'); +require('chai').should(); + +describe(cryptoUtil.createHash.name, function () { + it('creates a hash by given algorithm', function () { + const data = 'client-credentials-grant'; + const hash = cryptoUtil.createHash({ data, encoding: 'hex' }); + hash.should.equal('072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60'); + }); + it('should throw if data is missing', function () { + try { + cryptoUtil.createHash({}); + } catch (e) { + e.should.be.instanceOf(TypeError); + e.message.should.include('he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.'); + } + }); +});