diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b82f07..b7faf9f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - this is a breaking change, because **it removes callback support** for `OAuthServer` and your model implementation. - fixed missing await in calling generateAuthorizationCode in AuthorizeHandler +- validate scope as an array of strings ## 4.2.0 ### Fixed @@ -52,7 +53,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..c0b665af 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 diff --git a/docs/api/oauth2-server.rst b/docs/api/oauth2-server.rst index 48acf538..bdcf4ae6 100644 --- a/docs/api/oauth2-server.rst +++ b/docs/api/oauth2-server.rst @@ -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/docs/getting-started.rst b/docs/docs/getting-started.rst index ff2c1156..85bfd6e1 100644 --- a/docs/docs/getting-started.rst +++ b/docs/docs/getting-started.rst @@ -28,7 +28,7 @@ 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* (using Babel_). - Fully :rfc:`6749` and :rfc:`6750` compliant. - Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc. - Complete `test suite`_. diff --git a/docs/model/spec.rst b/docs/model/spec.rst index 953c2811..c4f0dc11 100644 --- a/docs/model/spec.rst +++ b/docs/model/spec.rst @@ -2,7 +2,7 @@ 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. +Each model function supports *promises*, *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,11 +14,6 @@ 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, using generators. getClient: function*() { yield somethingAsync(); @@ -41,7 +36,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 +59,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 +78,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 +100,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 +119,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 +139,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 +152,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 +170,28 @@ This model function is **required** if :ref:`OAuth2Server#authenticate() `. @@ -255,30 +242,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 +296,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 +314,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*. @@ -388,7 +371,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 +394,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 +441,7 @@ The return value (``client``) can carry additional properties that will be ignor .. _Model#getUser: -``getUser(username, password, [callback])`` +``getUser(username, password)`` =========================================== Invoked to retrieve a user using a username/password combination. @@ -480,8 +461,6 @@ This model function is **required** if the ``password`` grant is used. +------------+----------+---------------------------------------------------------------------+ | password | String | The user's password. | +------------+----------+---------------------------------------------------------------------+ -| [callback] | Function | Node-style callback to be used instead of the returned ``Promise``. | -+------------+----------+---------------------------------------------------------------------+ **Return value:** @@ -500,7 +479,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 +499,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 +519,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 +548,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 +625,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 +649,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 +662,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 +715,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 +737,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 +745,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 +768,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 +792,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 +800,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 +823,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 +847,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 +870,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,19 +884,14 @@ 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. @@ -951,7 +913,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 +921,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 +936,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/index.d.ts b/index.d.ts index 4c253606..d48f6c0d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -118,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. @@ -142,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 @@ -168,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. @@ -245,7 +245,7 @@ declare namespace OAuth2Server { * Invoked to generate a new access token. * */ - generateAccessToken?(client: Client, user: User, scope: string | string[]): 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. @@ -272,7 +272,7 @@ declare namespace OAuth2Server { * Optional, if a custom authenticateHandler is used or if there is no scope part of the request. * */ - verifyScope(token: Token, scope: string | string[]): Promise; + verifyScope?(token: Token, scope: string[]): Promise; } interface AuthorizationCodeModel extends BaseModel, RequestAuthenticationModel { @@ -280,13 +280,13 @@ declare namespace OAuth2Server { * Invoked to generate a new refresh token. * */ - generateRefreshToken?(client: Client, user: User, scope: string | string[]): Promise; + generateRefreshToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to generate a new authorization code. * */ - generateAuthorizationCode?(client: Client, user: User, scope: string | string[]): Promise; + generateAuthorizationCode?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve an existing authorization code previously saved through Model#saveAuthorizationCode(). @@ -314,8 +314,8 @@ declare namespace OAuth2Server { * Invoked to check if the requested scope is valid for a particular client/user combination. * */ - validateScope?(user: User, client: Client, scope: string | string[]): Promise; - + validateScope?(user: User, client: Client, scope: string[]): Promise; + /** * Invoked to check if the provided `redirectUri` is valid for a particular `client`. * @@ -328,7 +328,7 @@ declare namespace OAuth2Server { * Invoked to generate a new refresh token. * */ - generateRefreshToken?(client: Client, user: User, scope: string | string[]): Promise; + generateRefreshToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve a user using a username/password combination. @@ -340,7 +340,7 @@ declare namespace OAuth2Server { * Invoked to check if the requested scope is valid for a particular client/user combination. * */ - validateScope?(user: User, client: Client, scope: string | string[]): Promise; + validateScope?(user: User, client: Client, scope: string[]): Promise; } interface RefreshTokenModel extends BaseModel, RequestAuthenticationModel { @@ -348,7 +348,7 @@ declare namespace OAuth2Server { * Invoked to generate a new refresh token. * */ - generateRefreshToken?(client: Client, user: User, scope: string | string[]): Promise; + generateRefreshToken?(client: Client, user: User, scope: string[]): Promise; /** * Invoked to retrieve an existing refresh token previously saved through Model#saveToken(). @@ -374,7 +374,7 @@ declare namespace OAuth2Server { * Invoked to check if the requested scope is valid for a particular client/user combination. * */ - validateScope?(user: User, client: Client, scope: string | string[]): Promise; + validateScope?(user: User, client: Client, scope: string[]): Promise; } interface ExtensionModel extends BaseModel, RequestAuthenticationModel {} @@ -406,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; @@ -422,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; @@ -434,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 72fcc837..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) { @@ -73,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/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index dd6f7e25..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); } /** @@ -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 54945285..019b284d 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 a854eaf0..a02a5b9d 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. @@ -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); } /** 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/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js index 6b7a2898..5e71d4ab 100644 --- a/test/compliance/client-credential-workflow_test.js +++ b/test/compliance/client-credential-workflow_test.js @@ -90,14 +90,14 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { 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.equal(enabledScope); + 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.equal(enabledScope); + token.scope.should.eql(['read', 'write']); db.accessTokens.has(token.accessToken).should.equal(true); db.refreshTokens.has(token.refreshToken).should.equal(false); @@ -130,7 +130,7 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { token.accessToken.should.equal(accessToken); token.user.should.deep.equal(userDoc); token.client.should.deep.equal(clientDoc); - token.scope.should.equal(enabledScope); + token.scope.should.eql(['read', 'write']); response.status.should.equal(200); // there should be no information in the response as it @@ -139,4 +139,4 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { response.headers.should.deep.equal({}); }); }); -}); \ No newline at end of file +}); 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 22247d7a..d48c1ee0 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -144,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`'); } }); @@ -160,22 +160,22 @@ 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 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.equal(scope); + validated.should.eql(scope); }); it('accepts the scope, if the model accepts it', async function () { - const scope = 'some,scope,this,that'; + const scope = ['some,scope,this,that']; const user = { id: 123 }; const client = { id: 456 }; @@ -184,18 +184,18 @@ describe('AbstractGrantType integration', function() { // make sure the model received the correct args _user.should.deep.equal(user); _client.should.deep.equal(_client); - _scope.should.equal(scope); + _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.equal(scope); + validated.should.eql(scope); }); it('throws if the model rejects the scope', async function () { - const scope = 'some,scope,this,that'; + const scope = ['some,scope,this,that']; const user = { id: 123 }; const client = { id: 456 }; const returnTypes = [undefined, null, false, 0, '']; @@ -206,7 +206,7 @@ describe('AbstractGrantType integration', function() { // make sure the model received the correct args _user.should.deep.equal(user); _client.should.deep.equal(_client); - _scope.should.equal(scope); + _scope.should.eql(scope); return type; } 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 f4598bde..d705f397 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -88,7 +88,7 @@ describe('AuthorizationCodeGrantType integration', function() { e.message.should.equal('Missing parameter: `request`'); } }); - + it('should throw an error if `client` is invalid (not in code)', async function() { const client = { id: 1234 }; const model = { @@ -131,7 +131,7 @@ describe('AuthorizationCodeGrantType integration', function() { it('should return a token', async function() { const client = { id: 'foobar' }; - const scope = 'fooscope'; + const scope = ['fooscope']; const user = { name: 'foouser' }; const codeDoc = { authorizationCode: 12345, @@ -153,19 +153,19 @@ describe('AuthorizationCodeGrantType integration', function() { validateScope: async function (_user, _client, _scope) { _user.should.deep.equal(user); _client.should.deep.equal(client); - _scope.should.equal(scope); + _scope.should.eql(scope); return scope; }, generateAccessToken: async function (_client, _user, _scope) { _user.should.deep.equal(user); _client.should.deep.equal(client); - _scope.should.equal(scope); + _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.equal(scope); + _scope.should.eql(scope); return 'long-refresh-token-hash'; }, saveToken: async function (_token, _client, _user) { @@ -581,17 +581,17 @@ describe('AuthorizationCodeGrantType integration', function() { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); _token.refreshToken.should.be.a.sha256(); - _token.scope.should.equal('foo'); + _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') { + validateScope: function(_user= 'fallback', _client= 'fallback', _scope = ['fallback']) { _user.should.equal('fallback'); _client.should.equal('fallback'); - _scope.should.equal('fallback'); - return 'foo'; + _scope.should.eql(['fallback']); + return ['foo']; } }; const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); 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 a21b1a13..97d10055 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -94,7 +94,7 @@ describe('ClientCredentialsGrantType integration', function() { const token = {}; const client = { foo: 'bar' }; const user = { name: 'foo' }; - const scope = 'fooscope'; + const scope = ['fooscope']; const model = { getUserFromClient: async function(_client) { @@ -106,24 +106,24 @@ describe('ClientCredentialsGrantType integration', function() { _user.should.deep.equal(user); _token.accessToken.should.equal('long-access-token-hash'); _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.scope.should.equal(scope); + _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.equal(scope); + _scope.should.eql(scope); return scope; }, generateAccessToken: async function (_client, _user, _scope) { _user.should.deep.equal(user); _client.should.deep.equal(client); - _scope.should.equal(scope); + _scope.should.eql(scope); return 'long-access-token-hash'; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: { scope }, headers: {}, method: {}, query: {} }); + const request = new Request({ body: { scope: scope.join(' ') }, headers: {}, method: {}, query: {} }); const data = await grantType.handle(request, client); data.should.equal(token); @@ -218,7 +218,7 @@ describe('ClientCredentialsGrantType integration', function() { const model = { getUserFromClient: () => should.fail(), saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } + validateScope: function() { return ['foo']; } }; const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); const data = await grantType.saveToken(token); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index df1db899..20d2ac4f 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -92,7 +92,7 @@ describe('PasswordGrantType integration', function() { it('should return a token', async function() { const client = { id: 'foobar' }; - const scope = 'baz'; + const scope = ['baz']; const token = {}; const user = { id: 123456, @@ -109,19 +109,19 @@ describe('PasswordGrantType integration', function() { validateScope: async function(_user, _client, _scope) { _client.should.equal(client); _user.should.equal(user); - _scope.should.equal(scope); + _scope.should.eql(scope); return scope; }, generateAccessToken: async function (_client, _user, _scope) { _client.should.equal(client); _user.should.equal(user); - _scope.should.equal(scope); + _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.equal(scope); + _scope.should.eql(scope); return 'long-refresh-token-hash'; }, saveToken: async function(_token, _client, _user) { @@ -313,14 +313,14 @@ describe('PasswordGrantType integration', function() { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); _token.refreshToken.should.be.a.sha256(); - _token.scope.should.equal('foo'); + _token.scope.should.eql(['foo']); _client.should.equal('fallback'); _user.should.equal('fallback'); return token; }, - validateScope: async function(_scope = 'fallback') { - _scope.should.equal('fallback'); - return 'foo'; + validateScope: async function(_scope = ['fallback']) { + _scope.should.eql(['fallback']); + return ['foo']; } }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); 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 316b8064..0619fefd 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -116,7 +116,7 @@ describe('RefreshTokenGrantType integration', function() { accessToken: 'foo', client: { id: 123 }, user: { name: 'foo' }, - scope: 'read write', + scope: ['read', 'write'], refreshTokenExpiresAt: new Date( new Date() * 2) }; const model = { @@ -131,13 +131,13 @@ describe('RefreshTokenGrantType integration', function() { generateAccessToken: async function (_client, _user, _scope) { _user.should.deep.equal({ name: 'foo' }); _client.should.deep.equal({ id: 123 }); - _scope.should.equal('read write'); + _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.equal('read write'); + _scope.should.eql(['read', 'write']); return 'new-refresh-token'; }, saveToken: async function(_token, _client, _user) { @@ -506,7 +506,7 @@ describe('RefreshTokenGrantType integration', function() { saveToken: async function(_token, _client, _user) { _user.should.deep.equal(user); _client.should.deep.equal(client); - _token.scope.should.deep.equal(scope); + _token.scope.should.deep.eql(scope); _token.accessToken.should.be.a.sha256(); _token.refreshToken.should.be.a.sha256(); _token.accessTokenExpiresAt.should.be.instanceOf(Date); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 712dd7cd..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,10 +93,10 @@ describe('AuthenticateHandler integration', function() { addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foobar' + scope: ['foobar'] }); - grantType.scope.should.equal('foobar'); + grantType.scope.should.eql(['foobar']); }); }); @@ -254,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' }, @@ -522,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); @@ -539,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() { @@ -551,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); }); }); @@ -566,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'); }); @@ -576,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'); }); @@ -592,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'); }); @@ -602,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 1e4a515d..fbc3a9c4 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -464,7 +464,7 @@ describe('AuthorizeHandler integration', function() { return { authorizationCode: 12345, client }; }, validateScope: async function(_user, _client, _scope) { - _scope.should.equal('read'); + _scope.should.eql(['read']); return false; } }; @@ -629,7 +629,7 @@ describe('AuthorizeHandler integration', function() { it('should return the `code` if successful (full model implementation)', async function () { const user = { name: 'fooUser' }; const state = 'fooobarstatebaz'; - const scope = 'read'; + const scope = ['read']; const client = { id: 'client-1322132131', grants: ['authorization_code'], @@ -655,19 +655,19 @@ describe('AuthorizeHandler integration', function() { }, verifyScope: async function (_tokenDoc, _scope) { _tokenDoc.should.equal(accessTokenDoc); - _scope.should.equal(accessTokenDoc.scope); + _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.equal(scope); + _scope.should.eql(scope); return _scope; }, generateAuthorizationCode: async function (_client, _user, _scope) { _user.should.deep.equal(user); _client.should.deep.equal(client); - _scope.should.equal(scope); + _scope.should.eql(scope); return authorizationCode; }, saveAuthorizationCode: async function (code, _client, _user) { @@ -689,12 +689,12 @@ describe('AuthorizeHandler integration', function() { 'Authorization': `Bearer ${accessTokenDoc.accessToken}` }, method: {}, - query: { state, scope } + query: { state, scope: scope.join(' ') } }); const response = new Response({ body: {}, headers: {} }); const data = await handler.handle(request, response); - data.scope.should.equal(scope); + data.scope.should.eql(scope); data.client.should.deep.equal(client); data.user.should.deep.equal(user); data.expiresAt.should.be.instanceOf(Date); @@ -1096,7 +1096,7 @@ describe('AuthorizeHandler integration', function() { 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']); }); }); @@ -1110,7 +1110,7 @@ describe('AuthorizeHandler integration', function() { 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']); }); }); }); @@ -1421,7 +1421,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, 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); @@ -1454,7 +1454,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1469,7 +1469,7 @@ describe('AuthorizeHandler integration', function() { getClient: function() {}, saveAuthorizationCode: function() {} }; - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, 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 aad03356..7732bdc2 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -144,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/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..f2411765 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -45,17 +45,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/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() {