diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..7a7bb1570 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 820d105cc..68dd81f84 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,24 @@ -node_modules/ + +# IDE +/.idea +/.awcache +/.vscode/*.code-workspace +/.vscode/settings.json + +# misc +npm-debug.log + +# folders +/dist +/node_modules docs/_build/ __pycache__/ + +# files +.DS_Store *.pyc +/aio +# codecoverage +/.nyc_output +/coverage diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index 3c3629e64..000000000 --- a/.jshintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 441e9934f..000000000 --- a/.jshintrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "bitwise": true, - "curly": true, - "eqeqeq": true, - "esnext": true, - "expr": true, - "globalstrict": false, - "immed": true, - "indent": 2, - "jquery": true, - "latedef": false, - "mocha": true, - "newcap": true, - "noarg": true, - "node": true, - "noyield": true, - "predef": ["-Promise"], - "quotmark": "single", - "regexp": true, - "smarttabs": true, - "strict": false, - "trailing": false, - "undef": true, - "unused": true, - "white": false -} diff --git a/.nycrc b/.nycrc new file mode 100644 index 000000000..7d93c0d94 --- /dev/null +++ b/.nycrc @@ -0,0 +1,10 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "all": true, + "reporter": [ + "lcovonly", + "html", + "text", + "text-summary" + ] +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 000000000..6de9cff5b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "lf" +} diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 index 1ba9eefaa..9d70f4ecd --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,8 @@ language: node_js node_js: - - 4 - - 4.0 - - 6 - - 6.0 - - 7 - - 7.0 - 8 - - 8.0 + - 10 + - 12 sudo: false diff --git a/.vscode/README.md b/.vscode/README.md new file mode 100644 index 000000000..9656796b7 --- /dev/null +++ b/.vscode/README.md @@ -0,0 +1,22 @@ +# VSCode Configuration + +This folder contains opt-in [Workspace Settings](https://code.visualstudio.com/docs/getstarted/settings) and [Extension Recommendations](https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions) that our team recommends using when working on this repository. + +## Usage + +To use the recommended settings follow the steps below: + +- copy `.vscode/recommended-settings.json` to `.vscode/settings.json` +- restart the editor + +If you already have your custom workspace settings you should instead manually merge the file content. + +This isn't an automatic process so you will need to repeat it when settings are updated. + +To see the recommended extensions select "Extensions: Show Recommended Extensions" in the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette). + +## Editing `.vscode/recommended-settings.json` + +If you wish to add extra configuration items please keep in mind any settings you add here will be used by many users. + +Try to keep these settings to things that help facilitate the development process and avoid altering the user workflow whenever possible. diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..7fac8f753 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "ms-vscode.vscode-typescript-tslint-plugin" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..51fea6b85 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "--require", + "ts-node/register", + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "--recursive", + "${workspaceFolder}/test/**/*.spec.ts" + ], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} diff --git a/.vscode/recommended-settings.json b/.vscode/recommended-settings.json new file mode 100644 index 000000000..3fa8122cd --- /dev/null +++ b/.vscode/recommended-settings.json @@ -0,0 +1,39 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/node_modules": true, + "dist": true + }, + "files.watcherExclude": { + "**/.git/**": true, + "**/node_modules/**": true, + "**/dist/**": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true + }, + "editor.formatOnSave": false, + "typescript.format.enable": false, + "editor.formatOnPaste": false, + "[typescript]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll.tslint": true + } + }, + "[json]": { + "editor.formatOnSave": true + }, + "[jsonc]": { + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.formatOnSave": true + }, + "prettier.singleQuote": true, + "prettier.trailingComma": "all", + "prettier.printWidth": 80 +} diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index 2d784f6e6..d24eeea71 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ ## Changelog +### 5.0.0 +* BREAKING: Remove support for node v6 +* BREAKING: Remove support for callbacks only support native Promises +* new: Rewrote in TypeScript +* new: Switch from Eslint to Tslint +* new: added .vscode folder for recommend extensions and recommend setting required for development +* new: added suitable TypeScript Interfaces for various Objects + +### 4.0.0 +* BREAKING: Set server_error Code to 500 +* BREAKING: Remove support for node v4 +* new: Added revoke-handler to revoke access token +* new: Added implicit grant flow +* new: Switch from jshint to eslin +* fix: authorization_code grant should not be required in implicit flowt + +### 3.1.0 +* new: Added package-lock.json +* new: Extend model object with request context +* new: .npmignore tests +* fix: validate requested scope on authorize request +* fix: issue correct expiry dates for tokens +* fix: set numArgs for promisify of generateAuthorizationCode +* fix: Changed 'hasOwnProperty' call in Response +* docs: Ensure accessTokenExpiresAt is required +* docs: Add missing notice of breaking change for accessExpireLifetime to migration guide +* docs: Correct tokens time scale for 2.x to 3.x migration guide +* readme: Update Slack badge and link +* readme: Fix link to RFC6750 standard + +### 3.0.1 +* Updated dependencies + ### 3.0.0 * Complete re-write, with Promises and callback support * Dropped support for node v0.8, v0.10, v0.12 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md index b2a04b185..33a85901d 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ The *oauth2-server* module is framework-agnostic but there are several officiall ## Features -- Supports `authorization_code`, `client_credentials`, `refresh_token` and `password` grant, as well as *extension grants*, with scopes. +- Supports `authorization_code`, `client_credentials`, `refresh_token`, `implicit` 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)). -- Fully [RFC 6749](https://tools.ietf.org/html/rfc6749.html) and [RFC 6750](https://tools.ietf.org/html/rfc6749.html) compliant. +- 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. - Complete [test suite](https://github.com/oauthjs/node-oauth2-server/tree/master/test). @@ -63,6 +63,6 @@ npm test [travis-url]: https://travis-ci.org/oauthjs/node-oauth2-server [license-image]: https://img.shields.io/badge/license-MIT-blue.svg [license-url]: https://raw.githubusercontent.com/oauthjs/node-oauth2-server/master/LICENSE -[slack-image]: https://img.shields.io/badge/slack-join-E01563.svg -[slack-url]: https://oauthjs.slack.com +[slack-image]: https://slack.oauthjs.org/badge.svg +[slack-url]: https://slack.oauthjs.org diff --git a/TODO b/TODO new file mode 100644 index 000000000..723eeaabc --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ + +Todo: + ✔ Add a todo + ✔ A Basic Rewrite of library in TypeScript + ✔ A Basic Rewrite of tests in TypeScript + ☐ Add examples for nestjs, expressjs, koa and others + ☐ Add Migration guide + ☐ Review all Docs diff --git a/docs/api/oauth2-server.rst b/docs/api/oauth2-server.rst index 48acf538a..dcc5a4c3e 100644 --- a/docs/api/oauth2-server.rst +++ b/docs/api/oauth2-server.rst @@ -128,25 +128,27 @@ Authorizes a token request. **Arguments:** -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| Name | Type | Description | -+=========================================+=================+=============================================================================+ -| request | :doc:`request` | Request object. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [request.query.allowed=undefined] | String | ``'false'`` to deny the authorization request (see remarks section). | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| response | :doc:`response` | Response object. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options={}] | Object | Handler options. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options.authenticateHandler=undefined] | Object | The authenticate handler (see remarks section). | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [options.allowEmptyState=false] | Boolean | Allow clients to specify an empty ``state``. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ -| [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``. | -+-----------------------------------------+-----------------+-----------------------------------------------------------------------------+ ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| Name | Type | Description | ++=========================================+=================+================================================================================+ +| request | :doc:`request` | Request object. | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [request.query.allowed=undefined] | String | ``'false'`` to deny the authorization request (see remarks section). | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| response | :doc:`response` | Response object. | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [options={}] | Object | Handler options. | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [options.authenticateHandler=undefined] | Object | The authenticate handler (see remarks section). | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [options.allowEmptyState=false] | Boolean | Allow clients to specify an empty ``state``. | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [options.authorizationCodeLifetime=300] | Number | Lifetime of generated authorization codes in seconds (default = 5 minutes). | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [options.accessTokenLifetime=3600] | Number | Lifetime of generated implicit grant access token in seconds (default = 1 hr). | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ +| [callback=undefined] | Function | Node-style callback to be used instead of the returned ``Promise``. | ++-----------------------------------------+-----------------+--------------------------------------------------------------------------------+ **Return value:** diff --git a/docs/misc/migrating-v2-to-v3.rst b/docs/misc/migrating-v2-to-v3.rst index 5a3ec6431..9d03c8f28 100644 --- a/docs/misc/migrating-v2-to-v3.rst +++ b/docs/misc/migrating-v2-to-v3.rst @@ -28,19 +28,23 @@ The naming of the exposed middlewares has changed to match the OAuth2 _RFC_ more Server options -------------- -The following server options can be set when instantiating the OAuth service: +The following server options can be set when instantiating the OAuth service: * `addAcceptedScopesHeader`: **default true** Add the `X-Accepted-OAuth-Scopes` header with a list of scopes that will be accepted * `addAuthorizedScopesHeader`: **default true** Add the `X-OAuth-Scopes` header with a list of scopes that the user is authorized for * `allowBearerTokensInQueryString`: **default false** Determine if the bearer token can be included in the query string (i.e. `?access_token=`) for validation calls * `allowEmptyState`: **default false** If true, `state` can be empty or not passed. If false, `state` is required. -* `authorizationCodeLifetime`: **default 300** Default number of milliseconds that the authorization code is active for -* `accessTokenLifetime`: **default 3600** Default number of milliseconds that an access token is valid for -* `refreshTokenLifetime`: **default 1209600** Default number of milliseconds that a refresh token is valid for +* `authorizationCodeLifetime`: **default 300** Default number of seconds that the authorization code is active for +* `accessTokenLifetime`: **default 3600** Default number of seconds that an access token is valid for +* `refreshTokenLifetime`: **default 1209600** Default number of seconds that a refresh token is valid for * `allowExtendedTokenAttributes`: **default false** Allows additional attributes (such as `id_token`) to be included in token responses. -* `requireClientAuthentication`: **default true for all grant types** Allow ability to set client/secret authentication to `false` for a specific grant type. +* `requireClientAuthentication`: **default true for all grant types** Allow ability to set client/secret authentication to `false` for a specific grant type. -The following server options have been removed in v3.0.0 +The following server options have changed behavior in v3.0.0: + + * `accessTokenLifetime` can no longer be set to `null` to indicate a non-expiring token. The recommend alternative is to set accessTokenLifetime to a high value. + +The following server options have been removed in v3.0.0: * `grants`: **removed** (now returned by the `getClient` method). * `debug`: **removed** (not the responsibility of this module). @@ -56,7 +60,7 @@ Model specification * `generateAuthorizationCode()` is **optional** and should return a `String`. * `generateRefreshToken(client, user, scope)` is **optional** and should return a `String`. * `getAccessToken(token)` should return an object with: - + * `accessToken` (`String`) * `accessTokenExpiresAt` (`Date`) * `client` (`Object`), containing at least an `id` property that matches the supplied client @@ -71,7 +75,7 @@ Model specification * `user` (`Object`) * `getClient(clientId, clientSecret)` should return an object with, at minimum: - + * `redirectUris` (`Array`) * `grants` (`Array`) @@ -84,11 +88,11 @@ Model specification * `user` (`Object`) * `getUser(username, password)` should return an object: - + * No longer requires that `id` be returned. * `getUserFromClient(client)` should return an object: - + * No longer requires that `id` be returned. * `grantTypeAllowed()` was **removed**. You can instead: diff --git a/docs/model/overview.rst b/docs/model/overview.rst index 5e345abd0..f4363a560 100644 --- a/docs/model/overview.rst +++ b/docs/model/overview.rst @@ -58,6 +58,23 @@ Model functions used by the client credentials grant: - :ref:`Model#getUserFromClient` - :ref:`Model#saveToken` - :ref:`Model#validateScope` +-------- + +.. _ImplicitGrant: + +Implicit Grant +------------------------ + +See :rfc:`Section 4.2 of RFC 6749 <6749#section-4.2>`. + +An implicit grant is used to obtain access tokens optimised for public clients known to operate a particular redirection URI. Usually used for browser-based clients implemented in JavaScript. + +Model functions used by the implicit grant: + +- :ref:`Model#generateAccessToken` +- :ref:`Model#getClient` +- :ref:`Model#saveToken` +- :ref:`Model#validateScope` -------- diff --git a/docs/model/spec.rst b/docs/model/spec.rst index 341e50ee9..674f389a5 100644 --- a/docs/model/spec.rst +++ b/docs/model/spec.rst @@ -195,7 +195,7 @@ An ``Object`` representing the access token and associated data. +------------------------------+--------+--------------------------------------------------+ | token.accessToken | String | The access token passed to ``getAccessToken()``. | +------------------------------+--------+--------------------------------------------------+ -| [token.accessTokenExpiresAt] | Date | The expiry time of the access token. | +| token.accessTokenExpiresAt | Date | The expiry time of the access token. | +------------------------------+--------+--------------------------------------------------+ | [token.scope] | String | The authorized scope of the access token. | +------------------------------+--------+--------------------------------------------------+ @@ -399,6 +399,7 @@ This model function is **required** for all grant types. - ``authorization_code`` grant - ``client_credentials`` grant +- ``implicit`` grant - ``refresh_token`` grant - ``password`` grant @@ -553,6 +554,7 @@ This model function is **required** for all grant types. - ``authorization_code`` grant - ``client_credentials`` grant +- ``implicit`` grant - ``refresh_token`` grant - ``password`` grant @@ -865,6 +867,7 @@ This model function is **optional**. If not implemented, any scope is accepted. - ``authorization_code`` grant - ``client_credentials`` grant +- ``implicit`` grant - ``password`` grant **Arguments:** diff --git a/index.js b/index.js deleted file mode 100644 index f4f940ab5..000000000 --- a/index.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -/** - * Expose server and request/response classes. - */ - -exports = module.exports = require('./lib/server'); -exports.Request = require('./lib/request'); -exports.Response = require('./lib/response'); - -/** - * Export helpers for extension grants. - */ - -exports.AbstractGrantType = require('./lib/grant-types/abstract-grant-type'); - -/** - * Export error classes. - */ - -exports.AccessDeniedError = require('./lib/errors/access-denied-error'); -exports.InsufficientScopeError = require('./lib/errors/insufficient-scope-error'); -exports.InvalidArgumentError = require('./lib/errors/invalid-argument-error'); -exports.InvalidClientError = require('./lib/errors/invalid-client-error'); -exports.InvalidGrantError = require('./lib/errors/invalid-grant-error'); -exports.InvalidRequestError = require('./lib/errors/invalid-request-error'); -exports.InvalidScopeError = require('./lib/errors/invalid-scope-error'); -exports.InvalidTokenError = require('./lib/errors/invalid-token-error'); -exports.OAuthError = require('./lib/errors/oauth-error'); -exports.ServerError = require('./lib/errors/server-error'); -exports.UnauthorizedClientError = require('./lib/errors/unauthorized-client-error'); -exports.UnauthorizedRequestError = require('./lib/errors/unauthorized-request-error'); -exports.UnsupportedGrantTypeError = require('./lib/errors/unsupported-grant-type-error'); -exports.UnsupportedResponseTypeError = require('./lib/errors/unsupported-response-type-error'); - diff --git a/index.ts b/index.ts new file mode 100755 index 000000000..3cea1819c --- /dev/null +++ b/index.ts @@ -0,0 +1,10 @@ +export * from './lib/errors'; +export * from './lib/grant-types'; +export * from './lib/handlers'; +export * from './lib/interfaces'; +export { Request } from './lib/request'; +export { Response } from './lib/response'; +export * from './lib/response-types'; +export { OAuth2Server } from './lib/server'; +export * from './lib/token-types'; +export * from './lib/validator/is'; diff --git a/lib/constants/common.ts b/lib/constants/common.ts new file mode 100644 index 000000000..d8caa68f2 --- /dev/null +++ b/lib/constants/common.ts @@ -0,0 +1,12 @@ +export const MILLISECONDS_PER_SECOND = 1_000; +export const SECONDS_PER_MINUTE = 60; +export const MINUTES_PER_HOUR = 60; +export const HOURS_PER_DAY = 24; +export const DAYS_PER_WEEK = 7; +export const MONTHS_PER_YEAR = 12; + +export const SECOND = MILLISECONDS_PER_SECOND; +export const MINUTE = SECONDS_PER_MINUTE * SECOND; +export const HOUR = MINUTES_PER_HOUR * MINUTE; +export const DAY = HOURS_PER_DAY * HOUR; +export const WEEK = DAYS_PER_WEEK * DAY; diff --git a/lib/constants/index.ts b/lib/constants/index.ts new file mode 100644 index 000000000..d0b932366 --- /dev/null +++ b/lib/constants/index.ts @@ -0,0 +1 @@ +export * from './common'; diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js deleted file mode 100644 index d3ffc704a..000000000 --- a/lib/errors/access-denied-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The resource owner or authorization server denied the request" - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function AccessDeniedError(message, properties) { - properties = _.assign({ - code: 400, - name: 'access_denied' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(AccessDeniedError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = AccessDeniedError; diff --git a/lib/errors/access-denied-error.ts b/lib/errors/access-denied-error.ts new file mode 100755 index 000000000..d78e98d00 --- /dev/null +++ b/lib/errors/access-denied-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The resource owner or authorization server denied the request" + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class AccessDeniedError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'access_denied', ...properties }); + } +} diff --git a/lib/errors/index.ts b/lib/errors/index.ts new file mode 100644 index 000000000..24141cb6f --- /dev/null +++ b/lib/errors/index.ts @@ -0,0 +1,14 @@ +export { AccessDeniedError } from './access-denied-error'; +export { InsufficientScopeError } from './insufficient-scope-error'; +export { InvalidArgumentError } from './invalid-argument-error'; +export { InvalidClientError } from './invalid-client-error'; +export { InvalidGrantError } from './invalid-grant-error'; +export { InvalidRequestError } from './invalid-request-error'; +export { InvalidScopeError } from './invalid-scope-error'; +export { InvalidTokenError } from './invalid-token-error'; +export { OAuthError } from './oauth-error'; +export { ServerError } from './server-error'; +export { UnauthorizedClientError } from './unauthorized-client-error'; +export { UnauthorizedRequestError } from './unauthorized-request-error'; +export { UnsupportedGrantTypeError } from './unsupported-grant-type-error'; +export { UnsupportedResponseTypeError } from './unsupported-response-type-error'; diff --git a/lib/errors/insufficient-scope-error.js b/lib/errors/insufficient-scope-error.js deleted file mode 100644 index c6442eac6..000000000 --- a/lib/errors/insufficient-scope-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The request requires higher privileges than provided by the access token.." - * - * @see https://tools.ietf.org/html/rfc6750.html#section-3.1 - */ - -function InsufficientScopeError(message, properties) { - properties = _.assign({ - code: 403, - name: 'insufficient_scope' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InsufficientScopeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InsufficientScopeError; diff --git a/lib/errors/insufficient-scope-error.ts b/lib/errors/insufficient-scope-error.ts new file mode 100755 index 000000000..c63720a62 --- /dev/null +++ b/lib/errors/insufficient-scope-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The request requires higher privileges than provided by the access token." + * + * @see https://tools.ietf.org/html/rfc6750.html#section-3.1 + */ + +export class InsufficientScopeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 403, name: 'insufficient_scope', ...properties }); + } +} diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js deleted file mode 100644 index cb56d5a8b..000000000 --- a/lib/errors/invalid-argument-error.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - */ - -function InvalidArgumentError(message, properties) { - properties = _.assign({ - code: 500, - name: 'invalid_argument' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidArgumentError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidArgumentError; diff --git a/lib/errors/invalid-argument-error.ts b/lib/errors/invalid-argument-error.ts new file mode 100755 index 000000000..393dee964 --- /dev/null +++ b/lib/errors/invalid-argument-error.ts @@ -0,0 +1,12 @@ +import { OAuthError } from './oauth-error'; +/** + * Constructor. + * + * "The request requires valid argument." + * + */ +export class InvalidArgumentError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 500, name: 'invalid_argument', ...properties }); + } +} diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js deleted file mode 100644 index d95358c7c..000000000 --- a/lib/errors/invalid-client-error.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "Client authentication failed (e.g., unknown client, no client - * authentication included, or unsupported authentication method)" - * - * @see https://tools.ietf.org/html/rfc6749#section-5.2 - */ - -function InvalidClientError(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_client' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidClientError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidClientError; diff --git a/lib/errors/invalid-client-error.ts b/lib/errors/invalid-client-error.ts new file mode 100755 index 000000000..1b097b046 --- /dev/null +++ b/lib/errors/invalid-client-error.ts @@ -0,0 +1,16 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "Client authentication failed (e.g., unknown client, no client + * authentication included, or unsupported authentication method)" + * + * @see https://tools.ietf.org/html/rfc6749#section-5.2 + */ + +export class InvalidClientError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_client', ...properties }); + } +} diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js deleted file mode 100644 index 58d032e11..000000000 --- a/lib/errors/invalid-grant-error.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The provided authorization grant (e.g., authorization code, resource owner credentials) - * or refresh token is invalid, expired, revoked, does not match the redirection URI used - * in the authorization request, or was issued to another client." - * - * @see https://tools.ietf.org/html/rfc6749#section-5.2 - */ - -function InvalidGrantError(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_grant' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidGrantError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidGrantError; diff --git a/lib/errors/invalid-grant-error.ts b/lib/errors/invalid-grant-error.ts new file mode 100755 index 000000000..046ef1902 --- /dev/null +++ b/lib/errors/invalid-grant-error.ts @@ -0,0 +1,17 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The provided authorization grant (e.g., authorization code, resource owner credentials) + * or refresh token is invalid, expired, revoked, does not match the redirection URI used + * in the authorization request, or was issued to another client." + * + * @see https://tools.ietf.org/html/rfc6749#section-5.2 + */ + +export class InvalidGrantError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_grant', ...properties }); + } +} diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js deleted file mode 100644 index 4cf0a73b1..000000000 --- a/lib/errors/invalid-request-error.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The request is missing a required parameter, includes an invalid parameter value, - * includes a parameter more than once, or is otherwise malformed." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.2.2.1 - */ - -function InvalidRequest(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_request' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidRequest, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidRequest; diff --git a/lib/errors/invalid-request-error.ts b/lib/errors/invalid-request-error.ts new file mode 100755 index 000000000..d77b8683b --- /dev/null +++ b/lib/errors/invalid-request-error.ts @@ -0,0 +1,16 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The request is missing a required parameter, includes an invalid parameter value, + * includes a parameter more than once, or is otherwise malformed." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.2.2.1 + */ + +export class InvalidRequestError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_request', ...properties }); + } +} diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js deleted file mode 100644 index c3b287fc5..000000000 --- a/lib/errors/invalid-scope-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The requested scope is invalid, unknown, or malformed." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function InvalidScopeError(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_scope' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidScopeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidScopeError; diff --git a/lib/errors/invalid-scope-error.ts b/lib/errors/invalid-scope-error.ts new file mode 100755 index 000000000..9611d3aac --- /dev/null +++ b/lib/errors/invalid-scope-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The requested scope is invalid, unknown, or malformed." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class InvalidScopeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_scope', ...properties }); + } +} diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js deleted file mode 100644 index d7e7a8bfe..000000000 --- a/lib/errors/invalid-token-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The access token provided is expired, revoked, malformed, or invalid for other reasons." - * - * @see https://tools.ietf.org/html/rfc6750#section-3.1 - */ - -function InvalidTokenError(message, properties) { - properties = _.assign({ - code: 401, - name: 'invalid_token' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidTokenError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidTokenError; diff --git a/lib/errors/invalid-token-error.ts b/lib/errors/invalid-token-error.ts new file mode 100755 index 000000000..13f8a97df --- /dev/null +++ b/lib/errors/invalid-token-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor.invalid_token + * + * "The access token provided is expired, revoked, malformed, or invalid for other reasons." + * + * @see https://tools.ietf.org/html/rfc6750#section-3.1 + */ + +export class InvalidTokenError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 401, name: 'invalid_token', ...properties }); + } +} diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js deleted file mode 100644 index cd7d33930..000000000 --- a/lib/errors/oauth-error.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'); -var util = require('util'); -var statuses = require('statuses'); -/** - * Constructor. - */ - -function OAuthError(messageOrError, properties) { - var message = messageOrError instanceof Error ? messageOrError.message : messageOrError; - var error = messageOrError instanceof Error ? messageOrError : null; - if (_.isEmpty(properties)) - { - properties = {}; - } - - _.defaults(properties, { code: 500 }); - - if (error) { - properties.inner = error; - } - if (_.isEmpty(message)) { - message = statuses[properties.code]; - } - this.code = this.status = this.statusCode = properties.code; - this.message = message; - for (var key in properties) { - if (key !== 'code') { - this[key] = properties[key]; - } - } - Error.captureStackTrace(this, OAuthError); -} - -util.inherits(OAuthError, Error); - -/** - * Export constructor. - */ - -module.exports = OAuthError; diff --git a/lib/errors/oauth-error.ts b/lib/errors/oauth-error.ts new file mode 100755 index 000000000..3fd32de5c --- /dev/null +++ b/lib/errors/oauth-error.ts @@ -0,0 +1,32 @@ +import * as statuses from 'statuses'; + +export class OAuthError extends Error { + code: any; + status: any; + statusCode: any; + constructor(messageOrError: string | Error, properties: any = {}) { + super(); + let message = + messageOrError instanceof Error ? messageOrError.message : messageOrError; + const error = messageOrError instanceof Error ? messageOrError : undefined; + let props: any = {}; + props = properties; + props.code = props.code || 500; // default code 500 + + if (error) { + props.inner = error; + } + if (!message) { + message = statuses[props.code]; + } + this.code = this.status = this.statusCode = props.code; + this.message = message; + + const ignoreAttr = ['code', 'message']; + Object.keys(props) + .filter(key => !ignoreAttr.includes(key)) + .forEach(key => (this[key] = props[key])); + + Error.captureStackTrace(this, OAuthError); + } +} diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js deleted file mode 100644 index d193af39c..000000000 --- a/lib/errors/server-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function ServerError(message, properties) { - properties = _.assign({ - code: 503, - name: 'server_error' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(ServerError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = ServerError; diff --git a/lib/errors/server-error.ts b/lib/errors/server-error.ts new file mode 100755 index 000000000..bccc22a41 --- /dev/null +++ b/lib/errors/server-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * ServerError + * + * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class ServerError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 500, name: 'server_error', ...properties }); + } +} diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js deleted file mode 100644 index c05075d3d..000000000 --- a/lib/errors/unauthorized-client-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authenticated client is not authorized to use this authorization grant type." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function UnauthorizedClientError(message, properties) { - properties = _.assign({ - code: 400, - name: 'unauthorized_client' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnauthorizedClientError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnauthorizedClientError; diff --git a/lib/errors/unauthorized-client-error.ts b/lib/errors/unauthorized-client-error.ts new file mode 100755 index 000000000..14f3fa0ae --- /dev/null +++ b/lib/errors/unauthorized-client-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The authenticated client is not authorized to use this authorization grant type." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class UnauthorizedClientError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'unauthorized_client', ...properties }); + } +} diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js deleted file mode 100644 index ae7500d9c..000000000 --- a/lib/errors/unauthorized-request-error.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "If the request lacks any authentication information (e.g., the client - * was unaware that authentication is necessary or attempted using an - * unsupported authentication method), the resource server SHOULD NOT - * include an error code or other error information." - * - * @see https://tools.ietf.org/html/rfc6750#section-3.1 - */ - -function UnauthorizedRequestError(message, properties) { - properties = _.assign({ - code: 401, - name: 'unauthorized_request' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnauthorizedRequestError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnauthorizedRequestError; diff --git a/lib/errors/unauthorized-request-error.ts b/lib/errors/unauthorized-request-error.ts new file mode 100755 index 000000000..2de582c43 --- /dev/null +++ b/lib/errors/unauthorized-request-error.ts @@ -0,0 +1,18 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "If the request lacks any authentication information (e.g., the client + * was unaware that authentication is necessary or attempted using an + * unsupported authentication method), the resource server SHOULD NOT + * include an error code or other error information." + * + * @see https://tools.ietf.org/html/rfc6750#section-3.1 + */ + +export class UnauthorizedRequestError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 401, name: 'unauthorized_request', ...properties }); + } +} diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js deleted file mode 100644 index 28ca0ec04..000000000 --- a/lib/errors/unsupported-grant-type-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authorization grant type is not supported by the authorization server." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function UnsupportedGrantTypeError(message, properties) { - properties = _.assign({ - code: 400, - name: 'unsupported_grant_type' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnsupportedGrantTypeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnsupportedGrantTypeError; diff --git a/lib/errors/unsupported-grant-type-error.ts b/lib/errors/unsupported-grant-type-error.ts new file mode 100755 index 000000000..5174c6bce --- /dev/null +++ b/lib/errors/unsupported-grant-type-error.ts @@ -0,0 +1,19 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The authorization grant type is not supported by the authorization server." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class UnsupportedGrantTypeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { + code: 400, + name: 'unsupported_grant_type', + ...properties, + }); + } +} diff --git a/lib/errors/unsupported-response-type-error.js b/lib/errors/unsupported-response-type-error.js deleted file mode 100644 index 523cc449b..000000000 --- a/lib/errors/unsupported-response-type-error.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authorization server does not supported obtaining an - * authorization code using this method." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function UnsupportedResponseTypeError(message, properties) { - properties = _.assign({ - code: 400, - name: 'unsupported_response_type' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnsupportedResponseTypeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnsupportedResponseTypeError; diff --git a/lib/errors/unsupported-response-type-error.ts b/lib/errors/unsupported-response-type-error.ts new file mode 100755 index 000000000..9e7cd1471 --- /dev/null +++ b/lib/errors/unsupported-response-type-error.ts @@ -0,0 +1,20 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The authorization server does not supported obtaining an + * authorization code using this method." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class UnsupportedResponseTypeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { + code: 400, + name: 'unsupported_response_type', + ...properties, + }); + } +} diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js deleted file mode 100644 index be4259dec..000000000 --- a/lib/grant-types/abstract-grant-type.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidScopeError = require('../errors/invalid-scope-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var is = require('../validator/is'); -var tokenUtil = require('../utils/token-util'); - -/** - * Constructor. - */ - -function AbstractGrantType(options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; -} - -/** - * Generate access token. - */ - -AbstractGrantType.prototype.generateAccessToken = function(client, user, scope) { - if (this.model.generateAccessToken) { - return promisify(this.model.generateAccessToken, 3).call(this.model, client, user, scope) - .then(function(accessToken) { - return accessToken || tokenUtil.generateRandomToken(); - }); - } - - return tokenUtil.generateRandomToken(); -}; - -/** - * Generate refresh token. - */ - -AbstractGrantType.prototype.generateRefreshToken = function(client, user, scope) { - if (this.model.generateRefreshToken) { - return promisify(this.model.generateRefreshToken, 3).call(this.model, client, user, scope) - .then(function(refreshToken) { - return refreshToken || tokenUtil.generateRandomToken(); - }); - } - - return tokenUtil.generateRandomToken(); -}; - -/** - * Get access token expiration date. - */ - -AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); - - return expires; -}; - -/** - * Get refresh token expiration date. - */ - -AbstractGrantType.prototype.getRefreshTokenExpiresAt = function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); - - return expires; -}; - -/** - * Get scope from the request body. - */ - -AbstractGrantType.prototype.getScope = function(request) { - if (!is.nqschar(request.body.scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); - } - - return request.body.scope; -}; - -/** - * Validate requested scope. - */ -AbstractGrantType.prototype.validateScope = function(user, client, scope) { - if (this.model.validateScope) { - return promisify(this.model.validateScope, 3).call(this.model, user, client, scope) - .then(function (scope) { - if (!scope) { - throw new InvalidScopeError('Invalid scope: Requested scope is invalid'); - } - - return scope; - }); - } else { - return scope; - } -}; - -/** - * Export constructor. - */ - -module.exports = AbstractGrantType; diff --git a/lib/grant-types/abstract-grant-type.ts b/lib/grant-types/abstract-grant-type.ts new file mode 100755 index 000000000..8d9adea38 --- /dev/null +++ b/lib/grant-types/abstract-grant-type.ts @@ -0,0 +1,112 @@ +import { MILLISECONDS_PER_SECOND } from '../constants'; +import { InvalidArgumentError, InvalidScopeError } from '../errors'; +import { Client, Model, User } from '../interfaces'; +import { Request } from '../request'; +import * as tokenUtil from '../utils/token-util'; +import * as is from '../validator/is'; + +export class AbstractGrantType { + accessTokenLifetime: number; + model: Model; + refreshTokenLifetime: number; + alwaysIssueNewRefreshToken: boolean; + + constructor(options: any = {}) { + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; + } + + /** + * Generate access token. + */ + + async generateAccessToken(client?: Client, user?: User, scope?: string) { + if (this.model.generateAccessToken) { + const token = await this.model.generateAccessToken(client, user, scope); + + return token ? token : tokenUtil.GenerateRandomToken(); + } + + return tokenUtil.GenerateRandomToken(); + } + + /** + * Generate refresh token. + */ + + async generateRefreshToken(client?: Client, user?: User, scope?: string) { + if (this.model.generateRefreshToken) { + const token = await this.model.generateRefreshToken(client, user, scope); + + return token ? token : tokenUtil.GenerateRandomToken(); + } + + return tokenUtil.GenerateRandomToken(); + } + + /** + * Get access token expiration date. + */ + + getAccessTokenExpiresAt() { + return new Date( + Date.now() + this.accessTokenLifetime * MILLISECONDS_PER_SECOND, + ); + } + + /** + * Get refresh token expiration date. + */ + + getRefreshTokenExpiresAt() { + return new Date( + Date.now() + this.refreshTokenLifetime * MILLISECONDS_PER_SECOND, + ); + } + + /** + * Get scope from the request body. + */ + + getScope(request: Request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + + return request.body.scope; + } + + /** + * Validate requested scope. + */ + async validateScope(user: User, client: Client, scope: string) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + if (!validatedScope) { + throw new InvalidScopeError( + 'Invalid scope: Requested scope is invalid', + ); + } + + return validatedScope; + } + + return scope; + } +} diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js deleted file mode 100644 index 7eae70f8f..000000000 --- a/lib/grant-types/authorization-code-grant-type.js +++ /dev/null @@ -1,206 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var ServerError = require('../errors/server-error'); -var is = require('../validator/is'); -var util = require('util'); - -/** - * Constructor. - */ - -function AuthorizationCodeGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthorizationCode()`'); - } - - if (!options.model.revokeAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthorizationCode()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(AuthorizationCodeGrantType, AbstractGrantType); - -/** - * Handle authorization code grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - */ - -AuthorizationCodeGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - return Promise.bind(this) - .then(function() { - return this.getAuthorizationCode(request, client); - }) - .tap(function(code) { - return this.validateRedirectUri(request, code); - }) - .tap(function(code) { - return this.revokeAuthorizationCode(code); - }) - .then(function(code) { - return this.saveToken(code.user, client, code.authorizationCode, code.scope); - }); -}; - -/** - * Get the authorization code. - */ - -AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, client) { - if (!request.body.code) { - throw new InvalidRequestError('Missing parameter: `code`'); - } - - if (!is.vschar(request.body.code)) { - throw new InvalidRequestError('Invalid parameter: `code`'); - } - return promisify(this.model.getAuthorizationCode, 1).call(this.model, request.body.code) - .then(function(code) { - if (!code) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - if (!code.client) { - throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object'); - } - - if (!code.user) { - throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object'); - } - - if (code.client.id !== client.id) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - if (!(code.expiresAt instanceof Date)) { - throw new ServerError('Server error: `expiresAt` must be a Date instance'); - } - - if (code.expiresAt < new Date()) { - throw new InvalidGrantError('Invalid grant: authorization code has expired'); - } - - if (code.redirectUri && !is.uri(code.redirectUri)) { - throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI'); - } - - return code; - }); -}; - -/** - * Validate the redirect URI. - * - * "The authorization server MUST ensure that the redirect_uri parameter is - * present if the redirect_uri parameter was included in the initial - * authorization request as described in Section 4.1.1, and if included - * ensure that their values are identical." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - */ - - AuthorizationCodeGrantType.prototype.validateRedirectUri = function(request, code) { - if (!code.redirectUri) { - return; - } - - var redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (!is.uri(redirectUri)) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); - } - - if (redirectUri !== code.redirectUri) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is invalid'); - } - }; - -/** - * Revoke the authorization code. - * - * "The authorization code MUST expire shortly after it is issued to mitigate - * the risk of leaks. [...] If an authorization code is used more than once, - * the authorization server MUST deny the request." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 - */ - -AuthorizationCodeGrantType.prototype.revokeAuthorizationCode = function(code) { - return promisify(this.model.revokeAuthorizationCode, 1).call(this.model, code) - .then(function(status) { - if (!status) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - return code; - }); -}; - -/** - * Save token. - */ - -AuthorizationCodeGrantType.prototype.saveToken = function(user, client, authorizationCode, scope) { - var fns = [ - this.validateScope(user, client, scope), - this.generateAccessToken(client, user, scope), - this.generateRefreshToken(client, user, scope), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(scope, accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { - var token = { - accessToken: accessToken, - authorizationCode: authorizationCode, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, - scope: scope - }; - - return promisify(this.model.saveToken, 3).call(this.model, token, client, user); - }); -}; - -/** - * Export constructor. - */ - -module.exports = AuthorizationCodeGrantType; diff --git a/lib/grant-types/authorization-code-grant-type.ts b/lib/grant-types/authorization-code-grant-type.ts new file mode 100755 index 000000000..f00c82c71 --- /dev/null +++ b/lib/grant-types/authorization-code-grant-type.ts @@ -0,0 +1,202 @@ +import { AbstractGrantType } from '.'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../errors'; +import { AuthorizationCode, Client, Token, User } from '../interfaces'; +import { Request } from '../request'; +import * as is from '../validator/is'; + +export class AuthorizationCodeGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAuthorizationCode()`', + ); + } + + if (!options.model.revokeAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeAuthorizationCode()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Handle authorization code grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + const code = await this.getAuthorizationCode(request, client); + this.validateRedirectUri(request, code); + await this.revokeAuthorizationCode(code); + + return this.saveToken( + code.user, + client, + code.authorizationCode, + code.scope, + ); + } + + /** + * Get the authorization code. + */ + + async getAuthorizationCode(request: Request, client: Client) { + if (!request.body.code) { + throw new InvalidRequestError('Missing parameter: `code`'); + } + + if (!is.vschar(request.body.code)) { + throw new InvalidRequestError('Invalid parameter: `code`'); + } + + const code = await this.model.getAuthorizationCode(request.body.code); + if (!code) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + if (!code.client) { + throw new ServerError( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + } + + if (!code.user) { + throw new ServerError( + 'Server error: `getAuthorizationCode()` did not return a `user` object', + ); + } + + if (code.client.id !== client.id) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + if (!(code.expiresAt instanceof Date)) { + throw new ServerError( + 'Server error: `expiresAt` must be a Date instance', + ); + } + + if (code.expiresAt.getTime() < Date.now()) { + throw new InvalidGrantError( + 'Invalid grant: authorization code has expired', + ); + } + + if (code.redirectUri && !is.uri(code.redirectUri)) { + throw new InvalidGrantError( + 'Invalid grant: `redirect_uri` is not a valid URI', + ); + } + + return code; + } + + /** + * Validate the redirect URI. + * + * "The authorization server MUST ensure that the redirect_uri parameter is + * present if the redirect_uri parameter was included in the initial + * authorization request as described in Section 4.1.1, and if included + * ensure that their values are identical." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + + validateRedirectUri(request: Request, code: AuthorizationCode) { + if (!code.redirectUri) { + return; + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (!is.uri(redirectUri)) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + + if (redirectUri !== code.redirectUri) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is invalid', + ); + } + } + + /** + * Revoke the authorization code. + * + * "The authorization code MUST expire shortly after it is issued to mitigate + * the risk of leaks. [...] If an authorization code is used more than once, + * the authorization server MUST deny the request." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 + */ + + async revokeAuthorizationCode(code: AuthorizationCode) { + const status = await this.model.revokeAuthorizationCode(code); + if (!status) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + return code; + } + + /** + * Save token. + */ + + async saveToken( + user: User, + client: Client, + authorizationCode: string, + scope: string, + ) { + const accessScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = this.getRefreshTokenExpiresAt(); + + const token: Token = { + accessToken, + authorizationCode, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: accessScope, + } as any; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js deleted file mode 100644 index 138333e50..000000000 --- a/lib/grant-types/client-credentials-grant-type.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var util = require('util'); - -/** - * Constructor. - */ - -function ClientCredentialsGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getUserFromClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getUserFromClient()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(ClientCredentialsGrantType, AbstractGrantType); - -/** - * Handle client credentials grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 - */ - -ClientCredentialsGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - var scope = this.getScope(request); - - return Promise.bind(this) - .then(function() { - return this.getUserFromClient(client); - }) - .then(function(user) { - return this.saveToken(user, client, scope); - }); -}; - -/** - * Retrieve the user using client credentials. - */ - -ClientCredentialsGrantType.prototype.getUserFromClient = function(client) { - return promisify(this.model.getUserFromClient, 1).call(this.model, client) - .then(function(user) { - if (!user) { - throw new InvalidGrantError('Invalid grant: user credentials are invalid'); - } - - return user; - }); -}; - -/** - * Save token. - */ - -ClientCredentialsGrantType.prototype.saveToken = function(user, client, scope) { - var fns = [ - this.validateScope(user, client, scope), - this.generateAccessToken(client, user, scope), - this.getAccessTokenExpiresAt(client, user, scope) - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(scope, accessToken, accessTokenExpiresAt) { - var token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - scope: scope - }; - - return promisify(this.model.saveToken, 3).call(this.model, token, client, user); - }); -}; - -/** - * Export constructor. - */ - -module.exports = ClientCredentialsGrantType; diff --git a/lib/grant-types/client-credentials-grant-type.ts b/lib/grant-types/client-credentials-grant-type.ts new file mode 100755 index 000000000..80736bbac --- /dev/null +++ b/lib/grant-types/client-credentials-grant-type.ts @@ -0,0 +1,79 @@ +import { AbstractGrantType } from '.'; +import { InvalidArgumentError, InvalidGrantError } from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { Request } from '../request'; + +export class ClientCredentialsGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getUserFromClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getUserFromClient()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Handle client credentials grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const scope = this.getScope(request); + const user = await this.getUserFromClient(client); + + return this.saveToken(user, client, scope); + } + + /** + * Retrieve the user using client credentials. + */ + + async getUserFromClient(client: Client) { + const user = await this.model.getUserFromClient(client); + if (!user) { + throw new InvalidGrantError( + 'Invalid grant: user credentials are invalid', + ); + } + + return user; + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const accessScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + scope: accessScope, + } as Token; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/implicit-grant-type.ts b/lib/grant-types/implicit-grant-type.ts new file mode 100644 index 000000000..a37670482 --- /dev/null +++ b/lib/grant-types/implicit-grant-type.ts @@ -0,0 +1,63 @@ +import { AbstractGrantType } from '.'; +import { InvalidArgumentError } from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { Request } from '../request'; + +export class ImplicitGrantType extends AbstractGrantType { + scope: string; + user: User; + constructor(options: any = {}) { + super(options); + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + + if (!options.user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } + + this.scope = options.scope; + this.user = options.user; + } + + /** + * Handle implicit token grant. + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + return this.saveToken(this.user, client, this.scope); + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const validatedScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + scope: validatedScope, + } as Token; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/index.ts b/lib/grant-types/index.ts new file mode 100644 index 000000000..3d0ad0d4b --- /dev/null +++ b/lib/grant-types/index.ts @@ -0,0 +1,6 @@ +export { AbstractGrantType } from './abstract-grant-type'; +export { AuthorizationCodeGrantType } from './authorization-code-grant-type'; +export { ClientCredentialsGrantType } from './client-credentials-grant-type'; +export { ImplicitGrantType } from './implicit-grant-type'; +export { PasswordGrantType } from './password-grant-type'; +export { RefreshTokenGrantType } from './refresh-token-grant-type'; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js deleted file mode 100644 index b7f17935b..000000000 --- a/lib/grant-types/password-grant-type.js +++ /dev/null @@ -1,133 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var is = require('../validator/is'); -var util = require('util'); - -/** - * Constructor. - */ - -function PasswordGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getUser) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getUser()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(PasswordGrantType, AbstractGrantType); - -/** - * Retrieve the user from the model using a username/password combination. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 - */ - -PasswordGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - var scope = this.getScope(request); - - return Promise.bind(this) - .then(function() { - return this.getUser(request); - }) - .then(function(user) { - return this.saveToken(user, client, scope); - }); -}; - -/** - * Get user using a username/password combination. - */ - -PasswordGrantType.prototype.getUser = function(request) { - if (!request.body.username) { - throw new InvalidRequestError('Missing parameter: `username`'); - } - - if (!request.body.password) { - throw new InvalidRequestError('Missing parameter: `password`'); - } - - if (!is.uchar(request.body.username)) { - throw new InvalidRequestError('Invalid parameter: `username`'); - } - - if (!is.uchar(request.body.password)) { - throw new InvalidRequestError('Invalid parameter: `password`'); - } - - return promisify(this.model.getUser, 2).call(this.model, request.body.username, request.body.password) - .then(function(user) { - if (!user) { - throw new InvalidGrantError('Invalid grant: user credentials are invalid'); - } - - return user; - }); -}; - -/** - * Save token. - */ - -PasswordGrantType.prototype.saveToken = function(user, client, scope) { - var fns = [ - this.validateScope(user, client, scope), - this.generateAccessToken(client, user, scope), - this.generateRefreshToken(client, user, scope), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(scope, accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { - var token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, - scope: scope - }; - - return promisify(this.model.saveToken, 3).call(this.model, token, client, user); - }); -}; - -/** - * Export constructor. - */ - -module.exports = PasswordGrantType; diff --git a/lib/grant-types/password-grant-type.ts b/lib/grant-types/password-grant-type.ts new file mode 100755 index 000000000..ca07b06ed --- /dev/null +++ b/lib/grant-types/password-grant-type.ts @@ -0,0 +1,108 @@ +import { AbstractGrantType } from '.'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, +} from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { Request } from '../request'; +import * as is from '../validator/is'; + +export class PasswordGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getUser) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getUser()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Retrieve the user from the model using a username/password combination. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 + */ + + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const scope = this.getScope(request); + const user = await this.getUser(request); + + return this.saveToken(user, client, scope); + } + + /** + * Get user using a username/password combination. + */ + + async getUser(request: Request) { + if (!request.body.username) { + throw new InvalidRequestError('Missing parameter: `username`'); + } + + if (!request.body.password) { + throw new InvalidRequestError('Missing parameter: `password`'); + } + + if (!is.uchar(request.body.username)) { + throw new InvalidRequestError('Invalid parameter: `username`'); + } + + if (!is.uchar(request.body.password)) { + throw new InvalidRequestError('Invalid parameter: `password`'); + } + + const user = await this.model.getUser( + request.body.username, + request.body.password, + ); + if (!user) { + throw new InvalidGrantError( + 'Invalid grant: user credentials are invalid', + ); + } + + return user; + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const accessScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = this.getRefreshTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: accessScope, + } as Token; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js deleted file mode 100644 index 19f9010c2..000000000 --- a/lib/grant-types/refresh-token-grant-type.js +++ /dev/null @@ -1,180 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var ServerError = require('../errors/server-error'); -var is = require('../validator/is'); -var util = require('util'); - -/** - * Constructor. - */ - -function RefreshTokenGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getRefreshToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`'); - } - - if (!options.model.revokeToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(RefreshTokenGrantType, AbstractGrantType); - -/** - * Handle refresh token grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-6 - */ - -RefreshTokenGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - return Promise.bind(this) - .then(function() { - return this.getRefreshToken(request, client); - }) - .tap(function(token) { - return this.revokeToken(token); - }) - .then(function(token) { - return this.saveToken(token.user, client, token.scope); - }); -}; - -/** - * Get refresh token. - */ - -RefreshTokenGrantType.prototype.getRefreshToken = function(request, client) { - if (!request.body.refresh_token) { - throw new InvalidRequestError('Missing parameter: `refresh_token`'); - } - - if (!is.vschar(request.body.refresh_token)) { - throw new InvalidRequestError('Invalid parameter: `refresh_token`'); - } - - return promisify(this.model.getRefreshToken, 1).call(this.model, request.body.refresh_token) - .then(function(token) { - if (!token) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - if (!token.client) { - throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object'); - } - - if (!token.user) { - throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object'); - } - - if (token.client.id !== client.id) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { - throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); - } - - if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { - throw new InvalidGrantError('Invalid grant: refresh token has expired'); - } - - return token; - }); -}; - -/** - * Revoke the refresh token. - * - * @see https://tools.ietf.org/html/rfc6749#section-6 - */ - -RefreshTokenGrantType.prototype.revokeToken = function(token) { - if (this.alwaysIssueNewRefreshToken === false) { - return Promise.resolve(token); - } - - return promisify(this.model.revokeToken, 1).call(this.model, token) - .then(function(status) { - if (!status) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - return token; - }); -}; - -/** - * Save token. - */ - -RefreshTokenGrantType.prototype.saveToken = function(user, client, scope) { - var fns = [ - this.generateAccessToken(client, user, scope), - this.generateRefreshToken(client, user, scope), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { - var token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - scope: scope - }; - - if (this.alwaysIssueNewRefreshToken !== false) { - token.refreshToken = refreshToken; - token.refreshTokenExpiresAt = refreshTokenExpiresAt; - } - - return token; - }) - .then(function(token) { - return promisify(this.model.saveToken, 3).call(this.model, token, client, user) - .then(function(savedToken) { - return savedToken; - }); - }); -}; - -/** - * Export constructor. - */ - -module.exports = RefreshTokenGrantType; diff --git a/lib/grant-types/refresh-token-grant-type.ts b/lib/grant-types/refresh-token-grant-type.ts new file mode 100755 index 000000000..8e7d962e0 --- /dev/null +++ b/lib/grant-types/refresh-token-grant-type.ts @@ -0,0 +1,158 @@ +import { AbstractGrantType } from '.'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../errors'; +import { Client, RefreshToken, User } from '../interfaces'; +import { Request } from '../request'; +import * as is from '../validator/is'; + +export class RefreshTokenGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Handle refresh token grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const token = await this.getRefreshToken(request, client); + await this.revokeToken(token); + + return this.saveToken(token.user, client, token.scope); + } + + /** + * Get refresh token. + */ + + async getRefreshToken(request: Request, client: Client) { + if (!request.body.refresh_token) { + throw new InvalidRequestError('Missing parameter: `refresh_token`'); + } + + if (!is.vschar(request.body.refresh_token)) { + throw new InvalidRequestError('Invalid parameter: `refresh_token`'); + } + + const token = await this.model.getRefreshToken(request.body.refresh_token); + + if (!token) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if (!token.client) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + } + + if (!token.user) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + } + + if (token.client.id !== client.id) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if ( + token.refreshTokenExpiresAt && + !(token.refreshTokenExpiresAt instanceof Date) + ) { + throw new ServerError( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + } + + if ( + token.refreshTokenExpiresAt && + token.refreshTokenExpiresAt.getTime() < Date.now() + ) { + throw new InvalidGrantError('Invalid grant: refresh token has expired'); + } + + return token; + } + + /** + * Revoke the refresh token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async revokeToken(token: RefreshToken) { + if (this.alwaysIssueNewRefreshToken === false) { + return token; + } + + const status = await this.model.revokeToken(token); + if (!status) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + return token; + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = this.getRefreshTokenExpiresAt(); + + const token: any = { + accessToken, + accessTokenExpiresAt, + scope, + }; + + if (this.alwaysIssueNewRefreshToken !== false) { + token.refreshToken = refreshToken; + token.refreshTokenExpiresAt = refreshTokenExpiresAt; + } + + const savedToken = await this.model.saveToken(token, client, user); + + return savedToken; + } +} diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js deleted file mode 100644 index dc9117b27..000000000 --- a/lib/handlers/authenticate-handler.js +++ /dev/null @@ -1,263 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var InsufficientScopeError = require('../errors/insufficient-scope-error'); -var InvalidTokenError = require('../errors/invalid-token-error'); -var OAuthError = require('../errors/oauth-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var Request = require('../request'); -var Response = require('../response'); -var ServerError = require('../errors/server-error'); -var UnauthorizedRequestError = require('../errors/unauthorized-request-error'); - -/** - * Constructor. - */ - -function AuthenticateHandler(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getAccessToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`'); - } - - if (options.scope && undefined === options.addAcceptedScopesHeader) { - throw new InvalidArgumentError('Missing parameter: `addAcceptedScopesHeader`'); - } - - if (options.scope && undefined === options.addAuthorizedScopesHeader) { - throw new InvalidArgumentError('Missing parameter: `addAuthorizedScopesHeader`'); - } - - if (options.scope && !options.model.verifyScope) { - throw new InvalidArgumentError('Invalid argument: model does not implement `verifyScope()`'); - } - - this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; - this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; - this.allowBearerTokensInQueryString = options.allowBearerTokensInQueryString; - this.model = options.model; - this.scope = options.scope; -} - -/** - * Authenticate Handler. - */ - -AuthenticateHandler.prototype.handle = function(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - return Promise.bind(this) - .then(function() { - return this.getTokenFromRequest(request); - }) - .then(function(token) { - return this.getAccessToken(token); - }) - .tap(function(token) { - return this.validateAccessToken(token); - }) - .tap(function(token) { - if (!this.scope) { - return; - } - - return this.verifyScope(token); - }) - .tap(function(token) { - return this.updateResponse(response, token); - }) - .catch(function(e) { - // Include the "WWW-Authenticate" response header field if the client - // lacks any authentication information. - // - // @see https://tools.ietf.org/html/rfc6750#section-3.1 - if (e instanceof UnauthorizedRequestError) { - response.set('WWW-Authenticate', 'Bearer realm="Service"'); - } - - if (!(e instanceof OAuthError)) { - throw new ServerError(e); - } - - throw e; - }); -}; - -/** - * Get the token from the header or body, depending on the request. - * - * "Clients MUST NOT use more than one method to transmit the token in each request." - * - * @see https://tools.ietf.org/html/rfc6750#section-2 - */ - -AuthenticateHandler.prototype.getTokenFromRequest = function(request) { - var headerToken = request.get('Authorization'); - var queryToken = request.query.access_token; - var bodyToken = request.body.access_token; - - if (!!headerToken + !!queryToken + !!bodyToken > 1) { - throw new InvalidRequestError('Invalid request: only one authentication method is allowed'); - } - - if (headerToken) { - return this.getTokenFromRequestHeader(request); - } - - if (queryToken) { - return this.getTokenFromRequestQuery(request); - } - - if (bodyToken) { - return this.getTokenFromRequestBody(request); - } - - throw new UnauthorizedRequestError('Unauthorized request: no authentication given'); -}; - -/** - * Get the token from the request header. - * - * @see http://tools.ietf.org/html/rfc6750#section-2.1 - */ - -AuthenticateHandler.prototype.getTokenFromRequestHeader = function(request) { - var token = request.get('Authorization'); - var matches = token.match(/Bearer\s(\S+)/); - - if (!matches) { - throw new InvalidRequestError('Invalid request: malformed authorization header'); - } - - return matches[1]; -}; - -/** - * Get the token from the request query. - * - * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page - * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be - * passed in HTTP message headers or message bodies for which confidentiality measures - * are taken. Browsers, web servers, and other software may not adequately secure URLs - * in the browser history, web server logs, and other data structures. If bearer tokens - * are passed in page URLs, attackers might be able to steal them from the history data, - * logs, or other unsecured locations." - * - * @see http://tools.ietf.org/html/rfc6750#section-2.3 - */ - -AuthenticateHandler.prototype.getTokenFromRequestQuery = function(request) { - if (!this.allowBearerTokensInQueryString) { - throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); - } - - return request.query.access_token; -}; - -/** - * Get the token from the request body. - * - * "The HTTP request method is one for which the request-body has defined semantics. - * In particular, this means that the "GET" method MUST NOT be used." - * - * @see http://tools.ietf.org/html/rfc6750#section-2.2 - */ - -AuthenticateHandler.prototype.getTokenFromRequestBody = function(request) { - if (request.method === 'GET') { - throw new InvalidRequestError('Invalid request: token may not be passed in the body when using the GET verb'); - } - - if (!request.is('application/x-www-form-urlencoded')) { - throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'); - } - - return request.body.access_token; -}; - -/** - * Get the access token from the model. - */ - -AuthenticateHandler.prototype.getAccessToken = function(token) { - return promisify(this.model.getAccessToken, 1).call(this.model, token) - .then(function(accessToken) { - if (!accessToken) { - throw new InvalidTokenError('Invalid token: access token is invalid'); - } - - if (!accessToken.user) { - throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); - } - - return accessToken; - }); -}; - -/** - * Validate access token. - */ - -AuthenticateHandler.prototype.validateAccessToken = function(accessToken) { - if (!(accessToken.accessTokenExpiresAt instanceof Date)) { - throw new ServerError('Server error: `accessTokenExpiresAt` must be a Date instance'); - } - - if (accessToken.accessTokenExpiresAt < new Date()) { - throw new InvalidTokenError('Invalid token: access token has expired'); - } - - return accessToken; -}; - -/** - * Verify scope. - */ - -AuthenticateHandler.prototype.verifyScope = function(accessToken) { - return promisify(this.model.verifyScope, 2).call(this.model, accessToken, this.scope) - .then(function(scope) { - if (!scope) { - throw new InsufficientScopeError('Insufficient scope: authorized scope is insufficient'); - } - - return scope; - }); -}; - -/** - * Update response. - */ - -AuthenticateHandler.prototype.updateResponse = function(response, accessToken) { - if (this.scope && this.addAcceptedScopesHeader) { - response.set('X-Accepted-OAuth-Scopes', this.scope); - } - - if (this.scope && this.addAuthorizedScopesHeader) { - response.set('X-OAuth-Scopes', accessToken.scope); - } -}; - -/** - * Export constructor. - */ - -module.exports = AuthenticateHandler; diff --git a/lib/handlers/authenticate-handler.ts b/lib/handlers/authenticate-handler.ts new file mode 100755 index 000000000..67b30b0a1 --- /dev/null +++ b/lib/handlers/authenticate-handler.ts @@ -0,0 +1,273 @@ +import { + InsufficientScopeError, + InvalidArgumentError, + InvalidRequestError, + InvalidTokenError, + OAuthError, + ServerError, + UnauthorizedRequestError, +} from '../errors'; +import { Model, Token } from '../interfaces'; +import { Request } from '../request'; +import { Response } from '../response'; + +export class AuthenticateHandler { + addAcceptedScopesHeader: any; + addAuthorizedScopesHeader: any; + allowBearerTokensInQueryString: any; + model: Model; + scope: any; + constructor(options: any = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAccessToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + + if (options.scope && options.addAcceptedScopesHeader === undefined) { + throw new InvalidArgumentError( + 'Missing parameter: `addAcceptedScopesHeader`', + ); + } + + if (options.scope && options.addAuthorizedScopesHeader === undefined) { + throw new InvalidArgumentError( + 'Missing parameter: `addAuthorizedScopesHeader`', + ); + } + + if (options.scope && !options.model.verifyScope) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `verifyScope()`', + ); + } + + this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; + this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; + this.allowBearerTokensInQueryString = + options.allowBearerTokensInQueryString; + this.model = options.model; + this.scope = options.scope; + } + + /** + * Authenticate Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + // Extend model object with request + this.model.request = request; + + try { + let token = await this.getTokenFromRequest(request); + token = await this.getAccessToken(token); + this.validateAccessToken(token); + if (this.scope) { + await this.verifyScope(token); + } + this.updateResponse(response, token); + + return token; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // lacks any authentication information. + // + // @see https://tools.ietf.org/html/rfc6750#section-3.1 + if (e instanceof UnauthorizedRequestError) { + response.set('WWW-Authenticate', 'Bearer realm="Service"'); + } + + if (!(e instanceof OAuthError)) { + throw new ServerError(e); + } + + throw e; + } + } + + /** + * Get the token from the header or body, depending on the request. + * + * "Clients MUST NOT use more than one method to transmit the token in each request." + * + * @see https://tools.ietf.org/html/rfc6750#section-2 + */ + + getTokenFromRequest(request: Request) { + const headerToken = request.get('Authorization'); + const queryToken = request.query.access_token; + const bodyToken = request.body.access_token; + + if ([headerToken, queryToken, bodyToken].filter(Boolean).length > 1) { + throw new InvalidRequestError( + 'Invalid request: only one authentication method is allowed', + ); + } + + if (headerToken) { + return this.getTokenFromRequestHeader(request); + } + + if (queryToken) { + return this.getTokenFromRequestQuery(request); + } + + if (bodyToken) { + return this.getTokenFromRequestBody(request); + } + + throw new UnauthorizedRequestError( + 'Unauthorized request: no authentication given', + ); + } + + /** + * Get the token from the request header. + * + * @see http://tools.ietf.org/html/rfc6750#section-2.1 + */ + + getTokenFromRequestHeader(request: Request) { + const token = request.get('Authorization'); + const matches = token.match(/Bearer\s(\S+)/); + + if (!matches) { + throw new InvalidRequestError( + 'Invalid request: malformed authorization header', + ); + } + + return matches[1]; + } + + /** + * Get the token from the request query. + * + * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page + * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be + * passed in HTTP message headers or message bodies for which confidentiality measures + * are taken. Browsers, web servers, and other software may not adequately secure URLs + * in the browser history, web server logs, and other data structures. If bearer tokens + * are passed in page URLs, attackers might be able to steal them from the history data, + * logs, or other unsecured locations." + * + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + */ + + getTokenFromRequestQuery(request: Request) { + if (!this.allowBearerTokensInQueryString) { + throw new InvalidRequestError( + 'Invalid request: do not send bearer tokens in query URLs', + ); + } + + return request.query.access_token; + } + + /** + * Get the token from the request body. + * + * "The HTTP request method is one for which the request-body has defined semantics. + * In particular, this means that the "GET" method MUST NOT be used." + * + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + */ + + getTokenFromRequestBody(request: Request) { + if (request.method === 'GET') { + throw new InvalidRequestError( + 'Invalid request: token may not be passed in the body when using the GET verb', + ); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + + return request.body.access_token; + } + + /** + * Get the access token from the model. + */ + + async getAccessToken(token: string) { + const accessToken = await this.model.getAccessToken(token); + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (!accessToken.user) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + } + + return accessToken; + } + + /** + * Validate access token. + */ + + validateAccessToken(accessToken: Token) { + if (!(accessToken.accessTokenExpiresAt instanceof Date)) { + throw new ServerError( + 'Server error: `accessTokenExpiresAt` must be a Date instance', + ); + } + + if (accessToken.accessTokenExpiresAt.getTime() < Date.now()) { + throw new InvalidTokenError('Invalid token: access token has expired'); + } + + return accessToken; + } + + /** + * Verify scope. + */ + + async verifyScope(accessToken: Token) { + const scope = await this.model.verifyScope(accessToken, this.scope); + if (!scope) { + throw new InsufficientScopeError( + 'Insufficient scope: authorized scope is insufficient', + ); + } + + return scope; + } + + /** + * Update response. + */ + + updateResponse(response: Response, accessToken: Token) { + if (this.scope && this.addAcceptedScopesHeader) { + response.set('X-Accepted-OAuth-Scopes', this.scope); + } + + if (this.scope && this.addAuthorizedScopesHeader) { + response.set('X-OAuth-Scopes', accessToken.scope); + } + } +} diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js deleted file mode 100644 index 984136a8d..000000000 --- a/lib/handlers/authorize-handler.js +++ /dev/null @@ -1,332 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var AccessDeniedError = require('../errors/access-denied-error'); -var AuthenticateHandler = require('../handlers/authenticate-handler'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidClientError = require('../errors/invalid-client-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var InvalidScopeError = require('../errors/invalid-scope-error'); -var UnsupportedResponseTypeError = require('../errors/unsupported-response-type-error'); -var OAuthError = require('../errors/oauth-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var Request = require('../request'); -var Response = require('../response'); -var ServerError = require('../errors/server-error'); -var UnauthorizedClientError = require('../errors/unauthorized-client-error'); -var is = require('../validator/is'); -var tokenUtil = require('../utils/token-util'); -var url = require('url'); - -/** - * Response types. - */ - -var responseTypes = { - code: require('../response-types/code-response-type'), - //token: require('../response-types/token-response-type') -}; - -/** - * Constructor. - */ - -function AuthorizeHandler(options) { - options = options || {}; - - if (options.authenticateHandler && !options.authenticateHandler.handle) { - throw new InvalidArgumentError('Invalid argument: authenticateHandler does not implement `handle()`'); - } - - if (!options.authorizationCodeLifetime) { - throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); - } - - if (!options.model.saveAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); - } - - this.allowEmptyState = options.allowEmptyState; - this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); - this.authorizationCodeLifetime = options.authorizationCodeLifetime; - this.model = options.model; -} - -/** - * Authorize Handler. - */ - -AuthorizeHandler.prototype.handle = function(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - if ('false' === request.query.allowed) { - return Promise.reject(new AccessDeniedError('Access denied: user denied access to application')); - } - - var fns = [ - this.getAuthorizationCodeLifetime(), - this.getClient(request), - this.getUser(request, response) - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(expiresAt, client, user) { - var uri = this.getRedirectUri(request, client); - var scope; - var state; - var ResponseType; - - return Promise.bind(this) - .then(function() { - scope = this.getScope(request); - - return this.generateAuthorizationCode(client, user, scope); - }) - .then(function(authorizationCode) { - state = this.getState(request); - ResponseType = this.getResponseType(request); - - return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user); - }) - .then(function(code) { - var responseType = new ResponseType(code.authorizationCode); - var redirectUri = this.buildSuccessRedirectUri(uri, responseType); - - this.updateResponse(response, redirectUri, state); - - return code; - }) - .catch(function(e) { - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - var redirectUri = this.buildErrorRedirectUri(uri, e); - - this.updateResponse(response, redirectUri, state); - - throw e; - }); - }); -}; - -/** - * Generate authorization code. - */ - -AuthorizeHandler.prototype.generateAuthorizationCode = function(client, user, scope) { - if (this.model.generateAuthorizationCode) { - return promisify(this.model.generateAuthorizationCode).call(this.model, client, user, scope); - } - return tokenUtil.generateRandomToken(); -}; - -/** - * Get authorization code lifetime. - */ - -AuthorizeHandler.prototype.getAuthorizationCodeLifetime = function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.authorizationCodeLifetime); - return expires; -}; - -/** - * Get the client from the model. - */ - -AuthorizeHandler.prototype.getClient = function(request) { - var clientId = request.body.client_id || request.query.client_id; - - if (!clientId) { - throw new InvalidRequestError('Missing parameter: `client_id`'); - } - - if (!is.vschar(clientId)) { - throw new InvalidRequestError('Invalid parameter: `client_id`'); - } - - var redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (redirectUri && !is.uri(redirectUri)) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); - } - return promisify(this.model.getClient, 2).call(this.model, clientId, null) - .then(function(client) { - if (!client) { - throw new InvalidClientError('Invalid client: client credentials are invalid'); - } - - if (!client.grants) { - throw new InvalidClientError('Invalid client: missing client `grants`'); - } - - if (!_.includes(client.grants, 'authorization_code')) { - throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); - } - - if (!client.redirectUris || 0 === client.redirectUris.length) { - throw new InvalidClientError('Invalid client: missing client `redirectUri`'); - } - - if (redirectUri && !_.includes(client.redirectUris, redirectUri)) { - throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); - } - return client; - }); -}; - -/** - * Get scope from the request. - */ - -AuthorizeHandler.prototype.getScope = function(request) { - var scope = request.body.scope || request.query.scope; - - if (!is.nqschar(scope)) { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } - - return scope; -}; - -/** - * Get state from the request. - */ - -AuthorizeHandler.prototype.getState = function(request) { - var state = request.body.state || request.query.state; - - if (!this.allowEmptyState && !state) { - throw new InvalidRequestError('Missing parameter: `state`'); - } - - if (!is.vschar(state)) { - throw new InvalidRequestError('Invalid parameter: `state`'); - } - - return state; -}; - -/** - * Get user by calling the authenticate middleware. - */ - -AuthorizeHandler.prototype.getUser = function(request, response) { - if (this.authenticateHandler instanceof AuthenticateHandler) { - return this.authenticateHandler.handle(request, response).get('user'); - } - return promisify(this.authenticateHandler.handle, 2)(request, response).then(function(user) { - if (!user) { - throw new ServerError('Server error: `handle()` did not return a `user` object'); - } - - return user; - }); -}; - -/** - * Get redirect URI. - */ - -AuthorizeHandler.prototype.getRedirectUri = function(request, client) { - return request.body.redirect_uri || request.query.redirect_uri || client.redirectUris[0]; -}; - -/** - * Save authorization code. - */ - -AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) { - var code = { - authorizationCode: authorizationCode, - expiresAt: expiresAt, - redirectUri: redirectUri, - scope: scope - }; - return promisify(this.model.saveAuthorizationCode, 3).call(this.model, code, client, user); -}; - -/** - * Get response type. - */ - -AuthorizeHandler.prototype.getResponseType = function(request) { - var responseType = request.body.response_type || request.query.response_type; - - if (!responseType) { - throw new InvalidRequestError('Missing parameter: `response_type`'); - } - - if (!_.has(responseTypes, responseType)) { - throw new UnsupportedResponseTypeError('Unsupported response type: `response_type` is not supported'); - } - - return responseTypes[responseType]; -}; - -/** - * Build a successful response that redirects the user-agent to the client-provided url. - */ - -AuthorizeHandler.prototype.buildSuccessRedirectUri = function(redirectUri, responseType) { - return responseType.buildRedirectUri(redirectUri); -}; - -/** - * Build an error response that redirects the user-agent to the client-provided url. - */ - -AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) { - var uri = url.parse(redirectUri); - - uri.query = { - error: error.name - }; - - if (error.message) { - uri.query.error_description = error.message; - } - - return uri; -}; - -/** - * Update response with the redirect uri and the state parameter, if available. - */ - -AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, state) { - redirectUri.query = redirectUri.query || {}; - - if (state) { - redirectUri.query.state = state; - } - - response.redirect(url.format(redirectUri)); -}; - -/** - * Export constructor. - */ - -module.exports = AuthorizeHandler; diff --git a/lib/handlers/authorize-handler.ts b/lib/handlers/authorize-handler.ts new file mode 100755 index 000000000..93d67a6e9 --- /dev/null +++ b/lib/handlers/authorize-handler.ts @@ -0,0 +1,377 @@ +import * as url from 'url'; +import { AuthenticateHandler } from '.'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidScopeError, + OAuthError, + ServerError, + UnauthorizedClientError, + UnsupportedResponseTypeError, +} from '../errors'; +import { Client, Model, User } from '../interfaces'; +import { Request } from '../request'; +import { Response } from '../response'; +import { CodeResponseType, TokenResponseType } from '../response-types'; +import { hasOwnProperty } from '../utils/fn'; +import * as is from '../validator/is'; + +/** + * Response types. + */ + +const responseTypes = { + code: CodeResponseType, + token: TokenResponseType, +}; + +/** + * Constructor. + */ + +export class AuthorizeHandler { + options: any; + allowEmptyState: boolean; + authenticateHandler: any; + model: Model; + constructor(options: any = {}) { + if (options.authenticateHandler && !options.authenticateHandler.handle) { + throw new InvalidArgumentError( + 'Invalid argument: authenticateHandler does not implement `handle()`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + this.options = options; + this.allowEmptyState = options.allowEmptyState; + this.authenticateHandler = + options.authenticateHandler || new AuthenticateHandler(options); + this.model = options.model; + } + + /** + * Authorize Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.query.allowed === 'false') { + throw new AccessDeniedError( + 'Access denied: user denied access to application', + ); + } + + // Extend model object with request + this.model.request = request; + + const client = await this.getClient(request); + const user = await this.getUser(request, response); + + let scope: string; + let state: string; + let RequestedResponseType: any; + let responseType: any; + const uri = this.getRedirectUri(request, client); + try { + const requestedScope = this.getScope(request); + + const validScope = await this.validateScope(user, client, requestedScope); + scope = validScope; + state = this.getState(request); + RequestedResponseType = this.getResponseType(request, client); + responseType = new RequestedResponseType(this.options); + const codeOrAccessToken = await responseType.handle( + request, + client, + user, + uri, + scope, + ); + const redirectUri = this.buildSuccessRedirectUri(uri, responseType); + this.updateResponse(response, redirectUri, responseType, state); + + return codeOrAccessToken; + } catch (e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + + const redirectUri = this.buildErrorRedirectUri(uri, responseType, e); + + this.updateResponse(response, redirectUri, responseType, state); + + throw e; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request: Request) { + const clientId = request.body.client_id || request.query.client_id; + + if (!clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if (!is.vschar(clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (redirectUri && !is.uri(redirectUri)) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + + const client = await this.model.getClient(clientId); + if (!client) { + throw new InvalidClientError( + 'Invalid client: client credentials are invalid', + ); + } + + if (!client.grants) { + throw new InvalidClientError('Invalid client: missing client `grants`'); + } + + const responseType = + request.body.response_type || request.query.response_type; + const requestedGrantType = + responseType === 'token' ? 'implicit' : 'authorization_code'; + + if (!client.grants.includes(requestedGrantType)) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + if (!client.redirectUris || client.redirectUris.length === 0) { + throw new InvalidClientError( + 'Invalid client: missing client `redirectUri`', + ); + } + + if (redirectUri && !client.redirectUris.includes(redirectUri)) { + throw new InvalidClientError( + 'Invalid client: `redirect_uri` does not match client value', + ); + } + + return client; + } + + /** + * Validate requested scope. + */ + async validateScope(user: User, client: Client, scope: string) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + if (!validatedScope) { + throw new InvalidScopeError( + 'Invalid scope: Requested scope is invalid', + ); + } + + return validatedScope; + } + + return scope; + } + + /** + * Get scope from the request. + */ + + getScope(request: Request) { + const scope = request.body.scope || request.query.scope; + + if (!is.nqschar(scope)) { + throw new InvalidScopeError('Invalid parameter: `scope`'); + } + + return scope; + } + + /** + * Get state from the request. + */ + + getState(request: Request) { + const state = request.body.state || request.query.state; + + if (!this.allowEmptyState && !state) { + throw new InvalidRequestError('Missing parameter: `state`'); + } + + if (!is.vschar(state)) { + throw new InvalidRequestError('Invalid parameter: `state`'); + } + + return state; + } + + /** + * Get user by calling the authenticate middleware. + */ + + async getUser(request: Request, response: Response) { + if (this.authenticateHandler instanceof AuthenticateHandler) { + const data = await this.authenticateHandler.handle(request, response); + + return data.user; + } + + const user = await this.authenticateHandler.handle(request, response); + if (!user) { + throw new ServerError( + 'Server error: `handle()` did not return a `user` object', + ); + } + + return user; + } + + /** + * Get redirect URI. + */ + + getRedirectUri(request: Request, client: Client) { + return ( + request.body.redirect_uri || + request.query.redirect_uri || + client.redirectUris[0] + ); + } + + /** + * Get response type. + */ + + getResponseType(request: Request, client: Client) { + const responseType = + request.body.response_type || request.query.response_type; + + if (!responseType) { + throw new InvalidRequestError('Missing parameter: `response_type`'); + } + + if (!hasOwnProperty(responseTypes, responseType)) { + throw new UnsupportedResponseTypeError( + 'Unsupported response type: `response_type` is not supported', + ); + } + + if ( + responseType === 'token' && + (!client || !client.grants.includes('implicit')) + ) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + return responseTypes[responseType]; + } + + /** + * Build a successful response that redirects the user-agent to the client-provided url. + */ + + buildSuccessRedirectUri( + redirectUri: string, + responseType: CodeResponseType | TokenResponseType, + ) { + const uri = url.parse(redirectUri); + + return responseType.buildRedirectUri(uri); + } + + /** + * Build an error response that redirects the user-agent to the client-provided url. + */ + + buildErrorRedirectUri( + redirectUri: any, + responseType: CodeResponseType | TokenResponseType, + error: Error, + ) { + let uri = url.parse(redirectUri, true); + + if (responseType) { + uri = responseType.setRedirectUriParam(uri, 'error', error.name); + + if (error.message) { + uri = responseType.setRedirectUriParam( + uri, + 'error_description', + error.message, + ); + } + } else { + uri.query = { + error: error.name, + }; + + if (error.message) { + uri.query.error_description = error.message; + } + } + + return uri; + } + + /** + * Update response with the redirect uri and the state parameter, if available. + */ + + updateResponse( + response: Response, + redirectUri: any, + responseType: CodeResponseType | TokenResponseType, + state: any, + ) { + if (responseType && state) { + // tslint:disable-next-line:no-parameter-reassignment + redirectUri = responseType.setRedirectUriParam( + redirectUri, + 'state', + state, + ); + } else if (state) { + redirectUri.query = redirectUri.query || {}; + redirectUri.query.state = state; + } + + response.redirect(url.format(redirectUri)); + } +} diff --git a/lib/handlers/index.ts b/lib/handlers/index.ts new file mode 100644 index 000000000..7bdf800ee --- /dev/null +++ b/lib/handlers/index.ts @@ -0,0 +1,4 @@ +export { AuthenticateHandler } from './authenticate-handler'; +export { AuthorizeHandler } from './authorize-handler'; +export { RevokeHandler } from './revoke-handler'; +export { TokenHandler } from './token-handler'; diff --git a/lib/handlers/revoke-handler.ts b/lib/handlers/revoke-handler.ts new file mode 100644 index 000000000..0636f99fa --- /dev/null +++ b/lib/handlers/revoke-handler.ts @@ -0,0 +1,341 @@ +import * as auth from 'basic-auth'; +import { + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidTokenError, + OAuthError, + ServerError, +} from '../errors'; +import { Client, Model } from '../interfaces'; +import { Request } from '../request'; +import { Response } from '../response'; +import { oneSuccess } from '../utils/fn'; +import * as is from '../validator/is'; + +export class RevokeHandler { + model: Model; + constructor(options: any = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + + if (!options.model.getAccessToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + + this.model = options.model; + } + + /** + * Revoke Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.method !== 'POST') { + throw new InvalidRequestError('Invalid request: method must be POST'); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + // Extend model object with request + this.model.request = request; + + try { + const client = await this.getClient(request, response); + + return this.handleRevokeToken(request, client); + } catch (e) { + let error = e; + if (!(error instanceof OAuthError)) { + error = new ServerError(error); + } + /** + * All necessary information is conveyed in the response code. + * + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * @see https://tools.ietf.org/html/rfc7009#section-2.2 + */ + if (!(error instanceof InvalidTokenError)) { + this.updateErrorResponse(response, error); + } + + throw error; + } + } + + /** + * Revoke a refresh or access token. + * + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" + */ + + async handleRevokeToken(request: Request, client: Client) { + try { + let token = await this.getTokenFromRequest(request); + token = await oneSuccess([ + this.getAccessToken(token, client), + this.getRefreshToken(token, client), + ]); + + return this.revokeToken(token); + } catch (errors) { + throw errors; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request: Request, response: Response) { + const credentials = this.getClientCredentials(request); + + if (!credentials.clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if (!credentials.clientSecret) { + throw new InvalidRequestError('Missing parameter: `client_secret`'); + } + + if (!is.vschar(credentials.clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + if (!is.vschar(credentials.clientSecret)) { + throw new InvalidRequestError('Invalid parameter: `client_secret`'); + } + try { + const client = await this.model.getClient( + credentials.clientId, + credentials.clientSecret, + ); + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if (!client.grants) { + throw new ServerError('Server error: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError('Server error: `grants` must be an array'); + } + + return client; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if (e instanceof InvalidClientError && request.get('authorization')) { + response.set('WWW-Authenticate', 'Basic realm="Service"'); + + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + } + } + + /** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + getClientCredentials(request: Request) { + const credentials = auth(request as any); + + if (credentials) { + return { clientId: credentials.name, clientSecret: credentials.pass }; + } + + if (request.body.client_id && request.body.client_secret) { + return { + clientId: request.body.client_id, + clientSecret: request.body.client_secret, + }; + } + + throw new InvalidClientError( + 'Invalid client: cannot retrieve client credentials', + ); + } + + /** + * Get the token from the body. + * + * @see https://tools.ietf.org/html/rfc7009#section-2.1 + */ + + getTokenFromRequest(request: Request) { + const bodyToken = request.body.token; + + if (!bodyToken) { + throw new InvalidRequestError('Missing parameter: `token`'); + } + + return bodyToken; + } + + /** + * Get refresh token. + */ + + async getRefreshToken(token, client: Client) { + const refreshToken = await this.model.getRefreshToken(token); + if (!refreshToken) { + throw new InvalidTokenError('Invalid token: refresh token is invalid'); + } + + if (!refreshToken.client) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + } + + if (!refreshToken.user) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + } + + if (refreshToken.client.id !== client.id) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if ( + refreshToken.refreshTokenExpiresAt && + !(refreshToken.refreshTokenExpiresAt instanceof Date) + ) { + throw new ServerError( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + } + + if ( + refreshToken.refreshTokenExpiresAt && + refreshToken.refreshTokenExpiresAt.getTime() < Date.now() + ) { + throw new InvalidTokenError('Invalid token: refresh token has expired'); + } + + return refreshToken; + } + + /** + * Get the access token from the model. + */ + + async getAccessToken(token: string, client: Client) { + const accessToken = await this.model.getAccessToken(token); + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (!accessToken.client) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `client` object', + ); + } + + if (!accessToken.user) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + } + + if (accessToken.client.id !== client.id) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if ( + accessToken.accessTokenExpiresAt && + !(accessToken.accessTokenExpiresAt instanceof Date) + ) { + throw new ServerError('Server error: `expires` must be a Date instance'); + } + + if ( + accessToken.accessTokenExpiresAt && + accessToken.accessTokenExpiresAt.getTime() < Date.now() + ) { + throw new InvalidTokenError('Invalid token: access token has expired.'); + } + + return accessToken; + } + + /** + * Revoke the token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async revokeToken(token: any) { + const revokedToken = await this.model.revokeToken(token); + if (!revokedToken) { + throw new InvalidTokenError('Invalid token: token is invalid'); + } + + return revokedToken; + } + + /** + * Update response when an error is thrown. + */ + + updateErrorResponse(response: Response, error: OAuthError) { + response.body = { + error: error.name, + error_description: error.message, + }; + + response.status = error.code; + } +} diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js deleted file mode 100644 index feaad3f54..000000000 --- a/lib/handlers/token-handler.js +++ /dev/null @@ -1,297 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var BearerTokenType = require('../token-types/bearer-token-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidClientError = require('../errors/invalid-client-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var OAuthError = require('../errors/oauth-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var Request = require('../request'); -var Response = require('../response'); -var ServerError = require('../errors/server-error'); -var TokenModel = require('../models/token-model'); -var UnauthorizedClientError = require('../errors/unauthorized-client-error'); -var UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error'); -var auth = require('basic-auth'); -var is = require('../validator/is'); - -/** - * Grant types. - */ - -var grantTypes = { - authorization_code: require('../grant-types/authorization-code-grant-type'), - client_credentials: require('../grant-types/client-credentials-grant-type'), - password: require('../grant-types/password-grant-type'), - refresh_token: require('../grant-types/refresh-token-grant-type') -}; - -/** - * Constructor. - */ - -function TokenHandler(options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.refreshTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `refreshTokenLifetime`'); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.grantTypes = _.assign({}, grantTypes, options.extendedGrantTypes); - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; - this.requireClientAuthentication = options.requireClientAuthentication || {}; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false; -} - -/** - * Token Handler. - */ - -TokenHandler.prototype.handle = function(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - if (request.method !== 'POST') { - return Promise.reject(new InvalidRequestError('Invalid request: method must be POST')); - } - - if (!request.is('application/x-www-form-urlencoded')) { - return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')); - } - - return Promise.bind(this) - .then(function() { - return this.getClient(request, response); - }) - .then(function(client) { - return this.handleGrantType(request, client); - }) - .tap(function(data) { - var model = new TokenModel(data, {allowExtendedTokenAttributes: this.allowExtendedTokenAttributes}); - var tokenType = this.getTokenType(model); - - this.updateSuccessResponse(response, tokenType); - }).catch(function(e) { - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - - this.updateErrorResponse(response, e); - - throw e; - }); -}; - -/** - * Get the client from the model. - */ - -TokenHandler.prototype.getClient = function(request, response) { - var credentials = this.getClientCredentials(request); - var grantType = request.body.grant_type; - - if (!credentials.clientId) { - throw new InvalidRequestError('Missing parameter: `client_id`'); - } - - if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret) { - throw new InvalidRequestError('Missing parameter: `client_secret`'); - } - - if (!is.vschar(credentials.clientId)) { - throw new InvalidRequestError('Invalid parameter: `client_id`'); - } - - if (credentials.clientSecret && !is.vschar(credentials.clientSecret)) { - throw new InvalidRequestError('Invalid parameter: `client_secret`'); - } - - return promisify(this.model.getClient, 2).call(this.model, credentials.clientId, credentials.clientSecret) - .then(function(client) { - if (!client) { - throw new InvalidClientError('Invalid client: client is invalid'); - } - - if (!client.grants) { - throw new ServerError('Server error: missing client `grants`'); - } - - if (!(client.grants instanceof Array)) { - throw new ServerError('Server error: `grants` must be an array'); - } - - return client; - }) - .catch(function(e) { - // Include the "WWW-Authenticate" response header field if the client - // attempted to authenticate via the "Authorization" request header. - // - // @see https://tools.ietf.org/html/rfc6749#section-5.2. - if ((e instanceof InvalidClientError) && request.get('authorization')) { - response.set('WWW-Authenticate', 'Basic realm="Service"'); - - throw new InvalidClientError(e, { code: 401 }); - } - - throw e; - }); -}; - -/** - * Get client credentials. - * - * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, - * the `client_id` and `client_secret` can be embedded in the body. - * - * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 - */ - -TokenHandler.prototype.getClientCredentials = function(request) { - var credentials = auth(request); - var grantType = request.body.grant_type; - - if (credentials) { - return { clientId: credentials.name, clientSecret: credentials.pass }; - } - - if (request.body.client_id && request.body.client_secret) { - return { clientId: request.body.client_id, clientSecret: request.body.client_secret }; - } - - if (!this.isClientAuthenticationRequired(grantType)) { - if(request.body.client_id) { - return { clientId: request.body.client_id }; - } - } - - throw new InvalidClientError('Invalid client: cannot retrieve client credentials'); -}; - -/** - * Handle grant type. - */ - -TokenHandler.prototype.handleGrantType = function(request, client) { - var grantType = request.body.grant_type; - - if (!grantType) { - throw new InvalidRequestError('Missing parameter: `grant_type`'); - } - - if (!is.nchar(grantType) && !is.uri(grantType)) { - throw new InvalidRequestError('Invalid parameter: `grant_type`'); - } - - if (!_.has(this.grantTypes, grantType)) { - throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid'); - } - - if (!_.includes(client.grants, grantType)) { - throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); - } - - var accessTokenLifetime = this.getAccessTokenLifetime(client); - var refreshTokenLifetime = this.getRefreshTokenLifetime(client); - var Type = this.grantTypes[grantType]; - - var options = { - accessTokenLifetime: accessTokenLifetime, - model: this.model, - refreshTokenLifetime: refreshTokenLifetime, - alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken - }; - - return new Type(options) - .handle(request, client); -}; - -/** - * Get access token lifetime. - */ - -TokenHandler.prototype.getAccessTokenLifetime = function(client) { - return client.accessTokenLifetime || this.accessTokenLifetime; -}; - -/** - * Get refresh token lifetime. - */ - -TokenHandler.prototype.getRefreshTokenLifetime = function(client) { - return client.refreshTokenLifetime || this.refreshTokenLifetime; -}; - -/** - * Get token type. - */ - -TokenHandler.prototype.getTokenType = function(model) { - return new BearerTokenType(model.accessToken, model.accessTokenLifetime, model.refreshToken, model.scope, model.customAttributes); -}; - -/** - * Update response when a token is generated. - */ - -TokenHandler.prototype.updateSuccessResponse = function(response, tokenType) { - response.body = tokenType.valueOf(); - - response.set('Cache-Control', 'no-store'); - response.set('Pragma', 'no-cache'); -}; - -/** - * Update response when an error is thrown. - */ - -TokenHandler.prototype.updateErrorResponse = function(response, error) { - response.body = { - error: error.name, - error_description: error.message - }; - - response.status = error.code; -}; - -/** - * Given a grant type, check if client authentication is required - */ -TokenHandler.prototype.isClientAuthenticationRequired = function(grantType) { - if (Object.keys(this.requireClientAuthentication).length > 0) { - return (typeof this.requireClientAuthentication[grantType] !== 'undefined') ? this.requireClientAuthentication[grantType] : true; - } else { - return true; - } -}; - -/** - * Export constructor. - */ - -module.exports = TokenHandler; diff --git a/lib/handlers/token-handler.ts b/lib/handlers/token-handler.ts new file mode 100755 index 000000000..66d9ac363 --- /dev/null +++ b/lib/handlers/token-handler.ts @@ -0,0 +1,331 @@ +import * as auth from 'basic-auth'; +import { + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + OAuthError, + ServerError, + UnauthorizedClientError, + UnsupportedGrantTypeError, +} from '../errors'; +import { + AuthorizationCodeGrantType, + ClientCredentialsGrantType, + PasswordGrantType, + RefreshTokenGrantType, +} from '../grant-types'; +import { Client, Model } from '../interfaces'; +import { TokenModel } from '../models'; +import { Request } from '../request'; +import { Response } from '../response'; +import { BearerTokenType } from '../token-types'; +import { hasOwnProperty } from '../utils/fn'; +import * as is from '../validator/is'; + +/** + * Grant types. + */ + +const grantTypes = { + authorization_code: AuthorizationCodeGrantType, + client_credentials: ClientCredentialsGrantType, + password: PasswordGrantType, + refresh_token: RefreshTokenGrantType, +}; +export class TokenHandler { + accessTokenLifetime: any; + grantTypes: { [key: string]: any }; + model: Model; + refreshTokenLifetime: number; + allowExtendedTokenAttributes: boolean; + requireClientAuthentication: any; + alwaysIssueNewRefreshToken: boolean; + constructor(options: any = {}) { + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.refreshTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `refreshTokenLifetime`', + ); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.grantTypes = { ...grantTypes, ...options.extendedGrantTypes }; + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; + this.requireClientAuthentication = + options.requireClientAuthentication || {}; + this.alwaysIssueNewRefreshToken = + options.alwaysIssueNewRefreshToken !== false; + } + + /** + * Token Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.method !== 'POST') { + throw new InvalidRequestError('Invalid request: method must be POST'); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + + // Extend model object with request + this.model.request = request; + + try { + const client = await this.getClient(request, response); + const data = await this.handleGrantType(request, client); + const model = new TokenModel(data, { + allowExtendedTokenAttributes: this.allowExtendedTokenAttributes, + }); + const tokenType = this.getTokenType(model); + this.updateSuccessResponse(response, tokenType); + + return data; + } catch (e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + this.updateErrorResponse(response, e); + throw e; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request, response) { + const credentials = this.getClientCredentials(request); + const grantType = request.body.grant_type; + + if (!credentials.clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if ( + this.isClientAuthenticationRequired(grantType) && + !credentials.clientSecret + ) { + throw new InvalidRequestError('Missing parameter: `client_secret`'); + } + + if (!is.vschar(credentials.clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + if (credentials.clientSecret && !is.vschar(credentials.clientSecret)) { + throw new InvalidRequestError('Invalid parameter: `client_secret`'); + } + try { + const client = await this.model.getClient( + credentials.clientId, + credentials.clientSecret, + ); + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if (!client.grants) { + throw new ServerError('Server error: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError('Server error: `grants` must be an array'); + } + + return client; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if (e instanceof InvalidClientError && request.get('authorization')) { + response.set('WWW-Authenticate', 'Basic realm="Service"'); + + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + } + } + + /** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + getClientCredentials(request: Request) { + const credentials = auth(request as any); + const grantType = request.body.grant_type; + + if (credentials) { + return { + clientId: credentials.name, + clientSecret: credentials.pass, + }; + } + + if (request.body.client_id && request.body.client_secret) { + return { + clientId: request.body.client_id, + clientSecret: request.body.client_secret, + }; + } + + if ( + !this.isClientAuthenticationRequired(grantType) && + request.body.client_id + ) { + return { clientId: request.body.client_id }; + } + + throw new InvalidClientError( + 'Invalid client: cannot retrieve client credentials', + ); + } + + /** + * Handle grant type. + */ + + async handleGrantType(request: Request, client: Client) { + const grantType = request.body.grant_type; + + if (!grantType) { + throw new InvalidRequestError('Missing parameter: `grant_type`'); + } + + if (!is.nchar(grantType) && !is.uri(grantType)) { + throw new InvalidRequestError('Invalid parameter: `grant_type`'); + } + + if (!hasOwnProperty(this.grantTypes, grantType)) { + throw new UnsupportedGrantTypeError( + 'Unsupported grant type: `grant_type` is invalid', + ); + } + + if (!client.grants.includes(grantType)) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + const accessTokenLifetime = this.getAccessTokenLifetime(client); + const refreshTokenLifetime = this.getRefreshTokenLifetime(client); + const GrantType = this.grantTypes[grantType]; + + const options = { + accessTokenLifetime, + model: this.model, + refreshTokenLifetime, + alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, + }; + + return new GrantType(options).handle(request, client); + } + + /** + * Get access token lifetime. + */ + + getAccessTokenLifetime(client: Client) { + return client.accessTokenLifetime || this.accessTokenLifetime; + } + + /** + * Get refresh token lifetime. + */ + + getRefreshTokenLifetime(client: Client) { + return client.refreshTokenLifetime || this.refreshTokenLifetime; + } + + /** + * Get token type. + */ + + getTokenType(model: any) { + return new BearerTokenType( + model.accessToken, + model.accessTokenLifetime, + model.refreshToken, + model.scope, + model.customAttributes, + ); + } + + /** + * Update response when a token is generated. + */ + + updateSuccessResponse(response: Response, tokenType: BearerTokenType) { + response.body = tokenType.valueOf(); + + response.set('Cache-Control', 'no-store'); + response.set('Pragma', 'no-cache'); + } + + /** + * Update response when an error is thrown. + */ + + updateErrorResponse(response: Response, error: OAuthError) { + response.body = { + error: error.name, + error_description: error.message, + }; + + response.status = error.code; + } + + /** + * Given a grant type, check if client authentication is required. + */ + isClientAuthenticationRequired(grantType: string) { + if (Object.keys(this.requireClientAuthentication).length > 0) { + return typeof this.requireClientAuthentication[grantType] !== 'undefined' + ? this.requireClientAuthentication[grantType] + : true; + } + + return true; + } +} diff --git a/lib/interfaces/authorization-code.interface.ts b/lib/interfaces/authorization-code.interface.ts new file mode 100644 index 000000000..a1f781607 --- /dev/null +++ b/lib/interfaces/authorization-code.interface.ts @@ -0,0 +1,14 @@ +import { Client, User } from '.'; + +/** + * An interface representing the authorization code and associated data. + */ +export interface AuthorizationCode { + authorizationCode: string; + expiresAt: Date; + redirectUri: string; + scope?: string; + client: Client; + user: User; + [key: string]: any; +} diff --git a/lib/interfaces/client.interface.ts b/lib/interfaces/client.interface.ts new file mode 100644 index 000000000..dcdb074fe --- /dev/null +++ b/lib/interfaces/client.interface.ts @@ -0,0 +1,11 @@ +/** + * An interface representing the client and associated data + */ +export interface Client { + id: string; + redirectUris?: string | string[]; + grants: string | string[]; + accessTokenLifetime?: number; + refreshTokenLifetime?: number; + [key: string]: any; +} diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts new file mode 100644 index 000000000..a5cb2aeac --- /dev/null +++ b/lib/interfaces/index.ts @@ -0,0 +1,6 @@ +export { AuthorizationCode } from './authorization-code.interface'; +export { Client } from './client.interface'; +export { Model } from './model.interface'; +export { RefreshToken } from './refresh-token.interface'; +export { Token } from './token.interface'; +export { User } from './user.interface'; diff --git a/lib/interfaces/model.interface.ts b/lib/interfaces/model.interface.ts new file mode 100644 index 000000000..14e880e67 --- /dev/null +++ b/lib/interfaces/model.interface.ts @@ -0,0 +1,175 @@ +import { AuthorizationCode, Client, RefreshToken, Token, User } from '.'; +import { Request } from '../request'; + +export interface BaseModel { + request: Request; + /** + * Invoked to generate a new access token. + * + */ + generateAccessToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve a client using a client id or a + * client id/client secret combination, depending on the grant type. + * + */ + getClient(clientId: string, clientSecret?: string): Promise; + + /** + * Invoked to save an access token and optionally a refresh token, depending on the grant type. + * + */ + saveToken(token: Token, client: Client, user: User): Promise; +} + +export interface RequestAuthenticationModel { + /** + * Invoked to retrieve an existing access token previously saved through Model#saveToken(). + * + */ + getAccessToken(accessToken: string): Promise; + + /** + * Invoked during request authentication to check if + * the provided access token was authorized the requested scopes. + * + */ + verifyScope(token: Token, scope: string): Promise; +} + +export interface AuthorizationCodeModel + extends BaseModel, + RequestAuthenticationModel { + /** + * Invoked to generate a new refresh token. + * + */ + generateRefreshToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to generate a new authorization code. + * + */ + generateAuthorizationCode?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve an existing authorization + * code previously saved through Model#saveAuthorizationCode(). + * + */ + getAuthorizationCode(authorizationCode: string): Promise; + + /** + * Invoked to save an authorization code. + * + */ + saveAuthorizationCode( + code: AuthorizationCode, + client: Client, + user: User, + ): Promise; + + /** + * Invoked to revoke an authorization code. + * + */ + revokeAuthorizationCode(code: AuthorizationCode): Promise; + + /** + * Invoked to check if the requested scope is + * valid for a particular client/user combination. + * + */ + validateScope?(user: User, client: Client, scope: string): Promise; +} + +export interface PasswordModel extends BaseModel, RequestAuthenticationModel { + /** + * Invoked to generate a new refresh token. + * + */ + generateRefreshToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve a user using a + * username/password combination. + * + */ + getUser(username: string, password: string): Promise; + + /** + * Invoked to check if the requested scope + * is valid for a particular client/user combination. + * + */ + validateScope?(user: User, client: Client, scope: string): Promise; +} + +export interface RefreshTokenModel + extends BaseModel, + RequestAuthenticationModel { + /** + * Invoked to generate a new refresh token. + * + */ + generateRefreshToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve an existing refresh token previously saved through Model#saveToken(). + * + */ + getRefreshToken(refreshToken: string): Promise; + + /** + * Invoked to revoke a refresh token. + * + */ + revokeToken(token: RefreshToken | Token): Promise; +} + +export interface ClientCredentialsModel + extends BaseModel, + RequestAuthenticationModel { + /** + * Invoked to retrieve the user associated with the specified client. + * + */ + getUserFromClient(client: Client): Promise; + + /** + * Invoked to check if the requested scope is valid for a particular client/user combination. + * + */ + validateScope?(user: User, client: Client, scope: string): Promise; +} + +export interface ExtensionModel extends BaseModel, RequestAuthenticationModel {} + +export interface Model + extends BaseModel, + RequestAuthenticationModel, + AuthorizationCodeModel, + PasswordModel, + RefreshTokenModel, + ClientCredentialsModel {} diff --git a/lib/interfaces/refresh-token.interface.ts b/lib/interfaces/refresh-token.interface.ts new file mode 100644 index 000000000..71801f87d --- /dev/null +++ b/lib/interfaces/refresh-token.interface.ts @@ -0,0 +1,13 @@ +import { Client, User } from '.'; + +/** + * An interface representing the refresh token and associated data. + */ +export interface RefreshToken { + refreshToken: string; + refreshTokenExpiresAt?: Date; + scope?: string; + client: Client; + user: User; + [key: string]: any; +} diff --git a/lib/interfaces/token.interface.ts b/lib/interfaces/token.interface.ts new file mode 100644 index 000000000..ec696e687 --- /dev/null +++ b/lib/interfaces/token.interface.ts @@ -0,0 +1,15 @@ +import { Client, User } from '.'; + +/** + * An interface representing the token(s) and associated data. + */ +export interface Token { + accessToken: string; + accessTokenExpiresAt?: Date; + refreshToken?: string; + refreshTokenExpiresAt?: Date; + scope?: string; + client: Client; + user: User; + [key: string]: any; +} diff --git a/lib/interfaces/user.interface.ts b/lib/interfaces/user.interface.ts new file mode 100644 index 000000000..dc5afd5b0 --- /dev/null +++ b/lib/interfaces/user.interface.ts @@ -0,0 +1,7 @@ +/** + * An interface representing the user. + * A user object is completely transparent to oauth2-server and is simply used as input to model functions. + */ +export interface User { + [key: string]: any; +} diff --git a/lib/models/index.ts b/lib/models/index.ts new file mode 100644 index 000000000..34c69db1f --- /dev/null +++ b/lib/models/index.ts @@ -0,0 +1 @@ +export { TokenModel } from './token-model'; diff --git a/lib/models/token-model.js b/lib/models/token-model.js deleted file mode 100644 index c6bc3f8d4..000000000 --- a/lib/models/token-model.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); - -/** - * Constructor. - */ - -var modelAttributes = ['accessToken', 'accessTokenExpiresAt', 'refreshToken', 'refreshTokenExpiresAt', 'scope', 'client', 'user']; - -function TokenModel(data, options) { - data = data || {}; - - if (!data.accessToken) { - throw new InvalidArgumentError('Missing parameter: `accessToken`'); - } - - if (!data.client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - if (!data.user) { - throw new InvalidArgumentError('Missing parameter: `user`'); - } - - if (data.accessTokenExpiresAt && !(data.accessTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresAt`'); - } - - if (data.refreshTokenExpiresAt && !(data.refreshTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresAt`'); - } - - this.accessToken = data.accessToken; - this.accessTokenExpiresAt = data.accessTokenExpiresAt; - this.client = data.client; - this.refreshToken = data.refreshToken; - this.refreshTokenExpiresAt = data.refreshTokenExpiresAt; - this.scope = data.scope; - this.user = data.user; - - if (options && options.allowExtendedTokenAttributes) { - this.customAttributes = {}; - - for (var key in data) { - if (data.hasOwnProperty(key) && (modelAttributes.indexOf(key) < 0)) { - this.customAttributes[key] = data[key]; - } - } - } - - if(this.accessTokenExpiresAt) { - this.accessTokenLifetime = Math.floor((this.accessTokenExpiresAt - new Date()) / 1000); - } -} - -/** - * Export constructor. - */ - -module.exports = TokenModel; diff --git a/lib/models/token-model.ts b/lib/models/token-model.ts new file mode 100755 index 000000000..e5104901f --- /dev/null +++ b/lib/models/token-model.ts @@ -0,0 +1,82 @@ +import { MILLISECONDS_PER_SECOND } from '../constants'; +import { InvalidArgumentError } from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { hasOwnProperty } from '../utils/fn'; + +const modelAttributes = [ + 'accessToken', + 'accessTokenExpiresAt', + 'client', + 'refreshToken', + 'refreshTokenExpiresAt', + 'scope', + 'user', +]; + +export class TokenModel implements Token { + accessToken: string; + accessTokenExpiresAt?: Date; + refreshToken?: string; + refreshTokenExpiresAt?: Date; + scope?: string; + client: Client; + user: User; + customAttributes: {}; + accessTokenLifetime: number; + constructor(data: any = {}, options: any = {}) { + if (!data.accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } + + if (!data.client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + if (!data.user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } + + if ( + data.accessTokenExpiresAt && + !(data.accessTokenExpiresAt instanceof Date) + ) { + throw new InvalidArgumentError( + 'Invalid parameter: `accessTokenExpiresAt`', + ); + } + + if ( + data.refreshTokenExpiresAt && + !(data.refreshTokenExpiresAt instanceof Date) + ) { + throw new InvalidArgumentError( + 'Invalid parameter: `refreshTokenExpiresAt`', + ); + } + + this.accessToken = data.accessToken; + this.accessTokenExpiresAt = data.accessTokenExpiresAt; + this.client = data.client; + this.refreshToken = data.refreshToken; + this.refreshTokenExpiresAt = data.refreshTokenExpiresAt; + this.scope = data.scope; + this.user = data.user; + + if (options && options.allowExtendedTokenAttributes) { + this.customAttributes = {}; + + for (const key of Object.keys(data)) { + if (hasOwnProperty(data, key) && modelAttributes.indexOf(key) < 0) { + this.customAttributes[key] = data[key]; + } + } + } + + if (this.accessTokenExpiresAt) { + this.accessTokenLifetime = Math.floor( + (this.accessTokenExpiresAt.getTime() - new Date().getTime()) / + MILLISECONDS_PER_SECOND, + ); + } + } +} diff --git a/lib/request.js b/lib/request.js deleted file mode 100644 index 65e2db303..000000000 --- a/lib/request.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('./errors/invalid-argument-error'); -var typeis = require('type-is'); - -/** - * Constructor. - */ - -function Request(options) { - options = options || {}; - - if (!options.headers) { - throw new InvalidArgumentError('Missing parameter: `headers`'); - } - - if (!options.method) { - throw new InvalidArgumentError('Missing parameter: `method`'); - } - - if (!options.query) { - throw new InvalidArgumentError('Missing parameter: `query`'); - } - - this.body = options.body || {}; - this.headers = {}; - this.method = options.method; - this.query = options.query; - - // Store the headers in lower case. - for (var field in options.headers) { - if (options.headers.hasOwnProperty(field)) { - this.headers[field.toLowerCase()] = options.headers[field]; - } - } - - // Store additional properties of the request object passed in - for (var property in options) { - if (options.hasOwnProperty(property) && !this[property]) { - this[property] = options[property]; - } - } -} - -/** - * Get a request header. - */ - -Request.prototype.get = function(field) { - return this.headers[field.toLowerCase()]; -}; - -/** - * Check if the content-type matches any of the given mime type. - */ - -Request.prototype.is = function(types) { - if (!Array.isArray(types)) { - types = [].slice.call(arguments); - } - - return typeis(this, types) || false; -}; - -/** - * Export constructor. - */ - -module.exports = Request; diff --git a/lib/request.ts b/lib/request.ts new file mode 100755 index 000000000..5bfdfe467 --- /dev/null +++ b/lib/request.ts @@ -0,0 +1,77 @@ +import * as typeis from 'type-is'; +import { InvalidArgumentError } from './errors'; +import { hasOwnProperty } from './utils/fn'; + +export class Request { + body: any; + headers: any; + method: string; + query: any; + constructor( + options: { + body: any; + headers: any; + method: string; + query: any; + [key: string]: any; + } = {} as any, + ) { + if (!options.headers) { + throw new InvalidArgumentError('Missing parameter: `headers`'); + } + + if (!options.method) { + throw new InvalidArgumentError('Missing parameter: `method`'); + } + + if (typeof options.method !== 'string') { + throw new InvalidArgumentError('Invalid parameter: `method`'); + } + + if (!options.query) { + throw new InvalidArgumentError('Missing parameter: `query`'); + } + + this.body = options.body || {}; + this.headers = {}; + this.method = options.method.toUpperCase(); + this.query = options.query; + + // Store the headers in lower case. + for (const field of Object.keys(options.headers)) { + if (hasOwnProperty(options.headers, field)) { + this.headers[field.toLowerCase()] = options.headers[field]; + } + } + + // Store additional properties of the request object passed in + for (const property of Object.keys(options)) { + if (hasOwnProperty(options, property) && !this[property]) { + this[property] = options[property]; + } + } + } + + /** + * Get a request header. + */ + + get(field: string) { + return this.headers[field.toLowerCase()]; + } + + /** + * Check if the content-type matches any of the given mime type. + */ + public is(args: string[]): string | false; + public is(...args: string[]): string | false; + + is(...args) { + let types = args; + if (Array.isArray(types[0])) { + types = types[0]; + } + + return typeis(this as any, types) || false; + } +} diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js deleted file mode 100644 index 6eaf23a89..000000000 --- a/lib/response-types/code-response-type.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var url = require('url'); - -/** - * Constructor. - */ - -function CodeResponseType(code) { - if (!code) { - throw new InvalidArgumentError('Missing parameter: `code`'); - } - - this.code = code; -} - -/** - * Build redirect uri. - */ - -CodeResponseType.prototype.buildRedirectUri = function(redirectUri) { - if (!redirectUri) { - throw new InvalidArgumentError('Missing parameter: `redirectUri`'); - } - - var uri = url.parse(redirectUri, true); - - uri.query.code = this.code; - uri.search = null; - - return uri; -}; - -/** - * Export constructor. - */ - -module.exports = CodeResponseType; diff --git a/lib/response-types/code-response-type.ts b/lib/response-types/code-response-type.ts new file mode 100755 index 000000000..0d1b93e31 --- /dev/null +++ b/lib/response-types/code-response-type.ts @@ -0,0 +1,165 @@ +import { MILLISECONDS_PER_SECOND } from '../constants'; +import { InvalidArgumentError } from '../errors'; +import { AuthorizationCode, Client, Model, User } from '../interfaces'; +import { Request } from '../request'; +import * as tokenUtil from '../utils/token-util'; +export class CodeResponseType { + code: any; + authorizationCodeLifetime: number; + model: Model; + constructor(options: any = {}) { + if (!options.authorizationCodeLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `authorizationCodeLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.saveAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveAuthorizationCode()`', + ); + } + + this.code = undefined; + this.authorizationCodeLifetime = options.authorizationCodeLifetime; + this.model = options.model; + } + + /** + * Handle code response type. + */ + + async handle( + request: Request, + client: Client, + user: User, + uri: string, + scope: string, + ) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + if (!user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } + + if (!uri) { + throw new InvalidArgumentError('Missing parameter: `uri`'); + } + + const authorizationCode = await this.generateAuthorizationCode( + client, + user, + scope, + ); + const expiresAt = this.getAuthorizationCodeExpiresAt(client); + + const code = await this.saveAuthorizationCode( + authorizationCode, + expiresAt, + scope, + client, + uri, + user, + ); + this.code = code.authorizationCode; + + return code; + } + + /** + * Get authorization code expiration date. + */ + + getAuthorizationCodeExpiresAt(client: Client) { + const authorizationCodeLifetime = this.getAuthorizationCodeLifetime(client); + + return new Date( + Date.now() + authorizationCodeLifetime * MILLISECONDS_PER_SECOND, + ); + } + + /** + * Get authorization code lifetime. + */ + + getAuthorizationCodeLifetime(client: Client) { + return client.authorizationCodeLifetime || this.authorizationCodeLifetime; + } + + /** + * Save authorization code. + */ + + async saveAuthorizationCode( + authorizationCode: string, + expiresAt: Date, + scope: string, + client: Client, + redirectUri: any, + user: User, + ) { + const code = { + authorizationCode, + expiresAt, + redirectUri, + scope, + } as AuthorizationCode; + + return this.model.saveAuthorizationCode(code, client, user); + } + + /** + * Generate authorization code. + */ + + async generateAuthorizationCode(client: Client, user: User, scope: string) { + if (this.model.generateAuthorizationCode) { + return this.model.generateAuthorizationCode(client, user, scope); + } + + return tokenUtil.GenerateRandomToken(); + } + + /** + * Build redirect uri. + */ + + buildRedirectUri(redirectUri: any) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + redirectUri.search = undefined; + + return this.setRedirectUriParam(redirectUri, 'code', this.code); + } + + /** + * Set redirect uri parameter. + */ + + setRedirectUriParam(redirectUri: any, key: string, value: string) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + if (!key) { + throw new InvalidArgumentError('Missing parameter: `key`'); + } + + redirectUri.query = redirectUri.query || {}; + redirectUri.query[key] = value; + + return redirectUri; + } +} diff --git a/lib/response-types/index.ts b/lib/response-types/index.ts new file mode 100644 index 000000000..9866921eb --- /dev/null +++ b/lib/response-types/index.ts @@ -0,0 +1,2 @@ +export { CodeResponseType } from './code-response-type'; +export { TokenResponseType } from './token-response-type'; diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js deleted file mode 100644 index 2637f64cd..000000000 --- a/lib/response-types/token-response-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ServerError = require('../errors/server-error'); - -/** - * Constructor. - */ - -function TokenResponseType() { - throw new ServerError('Not implemented.'); -} - -/** - * Export constructor. - */ - -module.exports = TokenResponseType; diff --git a/lib/response-types/token-response-type.ts b/lib/response-types/token-response-type.ts new file mode 100755 index 000000000..8901a3228 --- /dev/null +++ b/lib/response-types/token-response-type.ts @@ -0,0 +1,97 @@ +import { InvalidArgumentError } from '../errors'; +import { ImplicitGrantType } from '../grant-types'; +import { Client, Model, User } from '../interfaces'; +import { Request } from '../request'; + +export class TokenResponseType { + accessToken: string; + accessTokenLifetime: number; + model: Model; + constructor(options: any = {}) { + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + this.accessToken = undefined; + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; + } + + /** + * Handle token response type. + */ + + async handle( + request: Request, + client: Client, + user: User, + uri: string, + scope: string, + ) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const accessTokenLifetime = this.getAccessTokenLifetime(client); + + const options = { + user, + scope, + model: this.model, + accessTokenLifetime, + }; + + const grantType = new ImplicitGrantType(options); + const token = await grantType.handle(request, client); + this.accessToken = token.accessToken; + + return token; + } + + /** + * Get access token lifetime. + */ + + getAccessTokenLifetime(client: Client) { + return client.accessTokenLifetime || this.accessTokenLifetime; + } + + /** + * Build redirect uri. + */ + + buildRedirectUri(redirectUri: any) { + return this.setRedirectUriParam( + redirectUri, + 'access_token', + this.accessToken, + ); + } + + /** + * Set redirect uri parameter. + */ + + setRedirectUriParam(redirectUri: any, key: string, value: any) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + if (!key) { + throw new InvalidArgumentError('Missing parameter: `key`'); + } + + redirectUri.hash = redirectUri.hash || ''; + redirectUri.hash += `${ + redirectUri.hash ? '&' : '' + }${key}=${encodeURIComponent(value)}`; + + return redirectUri; + } +} diff --git a/lib/response.js b/lib/response.js deleted file mode 100644 index 4e9d1ec69..000000000 --- a/lib/response.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -/** - * Constructor. - */ - -function Response(options) { - options = options || {}; - - this.body = options.body || {}; - this.headers = {}; - this.status = 200; - - // Store the headers in lower case. - for (var field in options.headers) { - if (options.headers.hasOwnProperty(field)) { - this.headers[field.toLowerCase()] = options.headers[field]; - } - } - - // Store additional properties of the response object passed in - for (var property in options) { - if (options.hasOwnProperty(property) && !this[property]) { - this[property] = options[property]; - } - } -} - -/** - * Get a response header. - */ - -Response.prototype.get = function(field) { - return this.headers[field.toLowerCase()]; -}; - -/** - * Redirect response. - */ - -Response.prototype.redirect = function(url) { - this.set('Location', url); - this.status = 302; -}; - -/** - * Set a response header. - */ - -Response.prototype.set = function(field, value) { - this.headers[field.toLowerCase()] = value; -}; - -/** - * Export constructor. - */ - -module.exports = Response; diff --git a/lib/response.ts b/lib/response.ts new file mode 100755 index 000000000..3128f10a0 --- /dev/null +++ b/lib/response.ts @@ -0,0 +1,51 @@ +import { hasOwnProperty } from './utils/fn'; + +export class Response { + body: any; + headers: any; + status: number; + constructor(options: any = {}) { + this.body = options.body || {}; + this.headers = {}; + this.status = 200; // OK + + // Store the headers in lower case. + for (const field of Object.keys(options.headers || {})) { + if (hasOwnProperty(options.headers, field)) { + this.headers[field.toLowerCase()] = options.headers[field]; + } + } + + // Store additional properties of the response object passed in. + for (const property of Object.keys(options)) { + if (hasOwnProperty(options, property) && !this[property]) { + this[property] = options[property]; + } + } + } + + /** + * Get a response header. + */ + + get(field: string) { + return this.headers[field.toLowerCase()]; + } + + /** + * Redirect response. + */ + + redirect(url: string) { + this.set('Location', url); + this.status = 302; // Found + } + + /** + * Set a response header. + */ + + set(field: string, value: string) { + this.headers[field.toLowerCase()] = value; + } +} diff --git a/lib/server.js b/lib/server.js deleted file mode 100644 index fba9ccf81..000000000 --- a/lib/server.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var AuthenticateHandler = require('./handlers/authenticate-handler'); -var AuthorizeHandler = require('./handlers/authorize-handler'); -var InvalidArgumentError = require('./errors/invalid-argument-error'); -var TokenHandler = require('./handlers/token-handler'); - -/** - * Constructor. - */ - -function OAuth2Server(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - this.options = options; -} - -/** - * Authenticate a token. - */ - -OAuth2Server.prototype.authenticate = function(request, response, options, callback) { - if (typeof options === 'string') { - options = {scope: options}; - } - - options = _.assign({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - allowBearerTokensInQueryString: false - }, this.options, options); - - return new AuthenticateHandler(options) - .handle(request, response) - .nodeify(callback); -}; - -/** - * Authorize a request. - */ - -OAuth2Server.prototype.authorize = function(request, response, options, callback) { - options = _.assign({ - allowEmptyState: false, - authorizationCodeLifetime: 5 * 60 // 5 minutes. - }, this.options, options); - - return new AuthorizeHandler(options) - .handle(request, response) - .nodeify(callback); -}; - -/** - * Create a token. - */ - -OAuth2Server.prototype.token = function(request, response, options, callback) { - options = _.assign({ - accessTokenLifetime: 60 * 60, // 1 hour. - refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. - allowExtendedTokenAttributes: false, - requireClientAuthentication: {} // defaults to true for all grant types - }, this.options, options); - - return new TokenHandler(options) - .handle(request, response) - .nodeify(callback); -}; - -/** - * Export constructor. - */ - -module.exports = OAuth2Server; diff --git a/lib/server.ts b/lib/server.ts new file mode 100755 index 000000000..3e81e441d --- /dev/null +++ b/lib/server.ts @@ -0,0 +1,100 @@ +import { HOUR, MINUTE, SECOND, WEEK } from './constants'; +import { InvalidArgumentError } from './errors'; +import { + AuthenticateHandler, + AuthorizeHandler, + RevokeHandler, + TokenHandler, +} from './handlers'; +import { Request } from './request'; +import { Response } from './response'; + +export class OAuth2Server { + options: any; + constructor(options: any = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + this.options = options; + } + + /** + * Authenticate a token. + */ + authenticate( + request: Request, + response?: Response, + scope?: string, + ): Promise; + authenticate( + request: Request, + response?: Response, + // tslint:disable-next-line:unified-signatures + options?: any, + ): Promise; + + async authenticate( + request: Request, + response?: Response, + options?: string | any, + ) { + let opt = options; + if (typeof opt === 'string') { + opt = { scope: opt }; + } + + opt = { + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + allowBearerTokensInQueryString: false, + ...this.options, + ...opt, + }; + + return new AuthenticateHandler(opt).handle(request, response); + } + + /** + * Authorize a request. + */ + + async authorize(request: Request, response: Response, options?: any) { + const opts = { + allowEmptyState: false, + accessTokenLifetime: HOUR / SECOND, + authorizationCodeLifetime: (MINUTE * 5) / SECOND, + ...this.options, + ...options, + }; + + return new AuthorizeHandler(opts).handle(request, response); + } + + /** + * Create a token. + */ + + async token(request: Request, response: Response, options?: any) { + const opts = { + accessTokenLifetime: HOUR / SECOND, // 1 hour in seconds. + refreshTokenLifetime: (WEEK * 2) / SECOND, // 2 weeks. + allowExtendedTokenAttributes: false, + requireClientAuthentication: {}, + ...this.options, + ...options, + }; + + return new TokenHandler(opts).handle(request, response); + } + + /** + * Revoke a token. + */ + + async revoke(request: Request, response: Response, options: any) { + const opt = { ...this.options, ...options }; + + return new RevokeHandler(opt).handle(request, response); + } +} diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js deleted file mode 100644 index 9124cb2e8..000000000 --- a/lib/token-types/bearer-token-type.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); - -/** - * Constructor. - */ - -function BearerTokenType(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes) { - if (!accessToken) { - throw new InvalidArgumentError('Missing parameter: `accessToken`'); - } - - this.accessToken = accessToken; - this.accessTokenLifetime = accessTokenLifetime; - this.refreshToken = refreshToken; - this.scope = scope; - - if (customAttributes) { - this.customAttributes = customAttributes; - } -} - -/** - * Retrieve the value representation. - */ - -BearerTokenType.prototype.valueOf = function() { - var object = { - access_token: this.accessToken, - token_type: 'Bearer' - }; - - if (this.accessTokenLifetime) { - object.expires_in = this.accessTokenLifetime; - } - - if (this.refreshToken) { - object.refresh_token = this.refreshToken; - } - - if (this.scope) { - object.scope = this.scope; - } - - for (var key in this.customAttributes) { - if (this.customAttributes.hasOwnProperty(key)) { - object[key] = this.customAttributes[key]; - } - } - return object; -}; - -/** - * Export constructor. - */ - -module.exports = BearerTokenType; diff --git a/lib/token-types/bearer-token-type.ts b/lib/token-types/bearer-token-type.ts new file mode 100755 index 000000000..dd08c4e72 --- /dev/null +++ b/lib/token-types/bearer-token-type.ts @@ -0,0 +1,61 @@ +import { InvalidArgumentError } from '../errors'; +import { hasOwnProperty } from '../utils/fn'; + +export class BearerTokenType { + accessToken: string; + accessTokenLifetime: number; + refreshToken: string; + scope: string; + customAttributes: any; + constructor( + accessToken: string, + accessTokenLifetime: number, + refreshToken: string, + scope: string, + customAttributes: any, + ) { + if (!accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } + + this.accessToken = accessToken; + this.accessTokenLifetime = accessTokenLifetime; + this.refreshToken = refreshToken; + this.scope = scope; + + if (customAttributes) { + this.customAttributes = customAttributes; + } + } + + /** + * Retrieve the value representation. + */ + + valueOf() { + const object: any = { + access_token: this.accessToken, + token_type: 'Bearer', + }; + + if (this.accessTokenLifetime) { + object.expires_in = this.accessTokenLifetime; + } + + if (this.refreshToken) { + object.refresh_token = this.refreshToken; + } + + if (this.scope) { + object.scope = this.scope; + } + + for (const key of Object.keys(this.customAttributes || {})) { + if (hasOwnProperty(this.customAttributes, key)) { + object[key] = this.customAttributes[key]; + } + } + + return object; + } +} diff --git a/lib/token-types/index.ts b/lib/token-types/index.ts new file mode 100644 index 000000000..7fa71bce4 --- /dev/null +++ b/lib/token-types/index.ts @@ -0,0 +1,2 @@ +export { BearerTokenType } from './bearer-token-type'; +export { MacTokenType } from './mac-token-type'; diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js deleted file mode 100644 index 9fdc600c6..000000000 --- a/lib/token-types/mac-token-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ServerError = require('../errors/server-error'); - -/** - * Constructor. - */ - -function MacTokenType() { - throw new ServerError('Not implemented.'); -} - -/** - * Export constructor. - */ - -module.exports = MacTokenType; diff --git a/lib/token-types/mac-token-type.ts b/lib/token-types/mac-token-type.ts new file mode 100755 index 000000000..8210f7c0d --- /dev/null +++ b/lib/token-types/mac-token-type.ts @@ -0,0 +1,8 @@ +import { ServerError } from '../errors'; + +// tslint:disable-next-line:no-unnecessary-class +export class MacTokenType { + constructor() { + throw new ServerError('Not implemented.'); + } +} diff --git a/lib/utils/fn.ts b/lib/utils/fn.ts new file mode 100644 index 000000000..ac5200c4d --- /dev/null +++ b/lib/utils/fn.ts @@ -0,0 +1,20 @@ +const identity = (v: any) => v; + +const reverser = (promise: Promise) => + promise.then(v => Promise.reject(v), identity); + +export const oneSuccess = (promises: Array>) => + Promise.all(promises.map(reverser)).then( + e => Promise.reject(AggregateError.from(e)), + identity, + ); + +export const hasOwnProperty = (o: any, k: string) => + Object.prototype.hasOwnProperty.call(o, k); + +export class AggregateError extends Array implements Error { + name = 'AggregateError'; + get message() { + return this.map(e => e.message).join('\n'); + } +} diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js deleted file mode 100644 index 0f73746bb..000000000 --- a/lib/utils/token-util.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var crypto = require('crypto'); -var randomBytes = require('bluebird').promisify(require('crypto').randomBytes); - -/** - * Export `TokenUtil`. - */ - -module.exports = { - - /** - * Generate random token. - */ - - generateRandomToken: function() { - return randomBytes(256).then(function(buffer) { - return crypto - .createHash('sha1') - .update(buffer) - .digest('hex'); - }); - } - -}; diff --git a/lib/utils/token-util.ts b/lib/utils/token-util.ts new file mode 100755 index 000000000..9e9270ed0 --- /dev/null +++ b/lib/utils/token-util.ts @@ -0,0 +1,16 @@ +import { createHash, randomBytes } from 'crypto'; +import { promisify } from 'util'; +const randomBytesPromise = promisify(randomBytes); + +/** + * Generate random token. + */ + +export const GenerateRandomToken = async () => { + const bytesSize = 256; + const buffer = await randomBytesPromise(bytesSize); + + return createHash('sha1') + .update(buffer) + .digest('hex'); +}; diff --git a/lib/validator/is.js b/lib/validator/is.js deleted file mode 100644 index 07af6cb64..000000000 --- a/lib/validator/is.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -/** - * Validation rules. - */ - -var rules = { - NCHAR: /^[\u002D|\u002E|\u005F|\w]+$/, - NQCHAR: /^[\u0021|\u0023-\u005B|\u005D-\u007E]+$/, - NQSCHAR: /^[\u0020-\u0021|\u0023-\u005B|\u005D-\u007E]+$/, - UNICODECHARNOCRLF: /^[\u0009|\u0020-\u007E|\u0080-\uD7FF|\uE000-\uFFFD|\u10000-\u10FFFF]+$/, - URI: /^[a-zA-Z][a-zA-Z0-9+.-]+:/, - VSCHAR: /^[\u0020-\u007E]+$/ -}; - -/** - * Export validation functions. - */ - -module.exports = { - - /** - * Validate if a value matches a unicode character. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - nchar: function(value) { - return rules.NCHAR.test(value); - }, - - /** - * Validate if a value matches a unicode character, including exclamation marks. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - nqchar: function(value) { - return rules.NQCHAR.test(value); - }, - - /** - * Validate if a value matches a unicode character, including exclamation marks and spaces. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - nqschar: function(value) { - return rules.NQSCHAR.test(value); - }, - - /** - * Validate if a value matches a unicode character excluding the carriage - * return and linefeed characters. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - uchar: function(value) { - return rules.UNICODECHARNOCRLF.test(value); - }, - - /** - * Validate if a value matches generic URIs. - * - * @see http://tools.ietf.org/html/rfc3986#section-3 - */ - uri: function(value) { - return rules.URI.test(value); - }, - - /** - * Validate if a value matches against the printable set of unicode characters. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - vschar: function(value) { - return rules.VSCHAR.test(value); - } -}; diff --git a/lib/validator/is.ts b/lib/validator/is.ts new file mode 100755 index 000000000..d9475878a --- /dev/null +++ b/lib/validator/is.ts @@ -0,0 +1,64 @@ +/** + * Validation rules. + */ + +const Rules = { + NCHAR: /^[\u002D|\u002E|\u005F|\w]+$/, + NQCHAR: /^[\u0021|\u0023-\u005B|\u005D-\u007E]+$/, + NQSCHAR: /^[\u0020-\u0021|\u0023-\u005B|\u005D-\u007E]+$/, + UNICODECHARNOCRLF: /^[\u0009|\u0020-\u007E|\u0080-\uD7FF|\uE000-\uFFFD|\u10000-\u10FFFF]+$/, + URI: /^[a-zA-Z][a-zA-Z0-9+.-]+:/, + VSCHAR: /^[\u0020-\u007E]+$/, +}; + +/** + * Export validation functions. + */ + +/** + * Validate if a value matches a unicode character. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const nchar = (value: string) => Rules.NCHAR.test(value); + +/** + * Validate if a value matches a unicode character, including exclamation marks. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const nqchar = (value: string) => Rules.NQCHAR.test(value); + +/** + * Validate if a value matches a unicode character, including exclamation marks and spaces. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const nqschar = (value: string) => Rules.NQSCHAR.test(value); + +/** + * Validate if a value matches a unicode character excluding the carriage + * and linefeed characters. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const uchar = (value: string) => Rules.UNICODECHARNOCRLF.test(value); + +/** + * Validate if a value matches generic URIs. + * + * @see http://tools.ietf.org/html/rfc3986#section-3 + */ +export const uri = (value: string) => Rules.URI.test(value); + +/** + * Validate if a value matches against the printable set of unicode characters. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const vschar = (value: string) => Rules.VSCHAR.test(value); diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 000000000..4862bd097 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3124 @@ +{ + "name": "oauth2-server", + "version": "5.0.0-dev.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", + "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.9.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", + "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.9.5" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-imports": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", + "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-transforms": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", + "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.6", + "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/template": "^7.8.6", + "@babel/types": "^7.9.0", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-replace-supers": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", + "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/helper-simple-access": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", + "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true + }, + "@babel/helpers": { + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", + "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0" + } + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", + "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", + "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.5", + "@babel/helper-function-name": "^7.9.5", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.0", + "@babel/types": "^7.9.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/types": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", + "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@istanbuljs/nyc-config-typescript": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.1.tgz", + "integrity": "sha512-/gz6LgVpky205LuoOfwEZmnUtaSmdk0QIMcNFj9OvxhiMhPpKftMgZmGN7jNj7jR+lr8IB1Yks3QSSSNSxfoaQ==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.2.tgz", + "integrity": "sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz", + "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/basic-auth": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.3.tgz", + "integrity": "sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/mocha": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", + "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "dev": true + }, + "@types/node": { + "version": "11.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.3.tgz", + "integrity": "sha512-5RzvXVietaB8S4dwDjxjltAOHtTO87fiksjqjWGZih97j6KSrdCDaRfmYMNrgrLM87odGBrsTHAl6N3fLraQaw==", + "dev": true + }, + "@types/sinon": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.0.tgz", + "integrity": "sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", + "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", + "dev": true + }, + "@types/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-4zJN5gJH+Km6hA36z8MnOKas6EU0qwxItTXNijYDPuZUsSk4EpIAB56fwnxZIhi3tHx42J7wqNdQTqt49Ar9FQ==", + "dev": true + }, + "@types/type-is": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", + "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", + "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "just-extend": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz", + "integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "chokidar": "3.3.0", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.3", + "ms": "2.1.1", + "node-environment-flags": "1.0.6", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz", + "integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "nyc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.1.tgz", + "integrity": "sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pidtree": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", + "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", + "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "dev": true + }, + "shx": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.2.tgz", + "integrity": "sha512-aS0mWtW3T2sHAenrSrip2XGv39O9dXIFUqxAEWHEOS1ePtGIBavdPJY1kE2IHl14V/4iCbUiNDPGdyYTtmhSoA==", + "dev": true, + "requires": { + "es6-object-assign": "^1.0.3", + "minimist": "^1.2.0", + "shelljs": "^0.8.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sinon": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz", + "integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.2", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.0.3", + "diff": "^4.0.2", + "nise": "^4.0.1", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.padend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", + "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.4.3", + "function-bind": "^1.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + } + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + } + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-node": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.0.tgz", + "integrity": "sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tslint": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.1.tgz", + "integrity": "sha512-kd6AQ/IgPRpLn6g5TozqzPdGNZ0q0jtXW4//hRcj10qLYBaa3mTUU2y2MCG+RXZm8Zx+KZi0eA+YCrMyNlF4UA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.10.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 6992ef15e..f72e94ab6 --- a/package.json +++ b/package.json @@ -1,46 +1,91 @@ { "name": "oauth2-server", "description": "Complete, framework-agnostic, compliant and well tested module for implementing an OAuth2 Server in node.js", - "version": "3.0.0", + "version": "5.0.0-dev.0", "keywords": [ "oauth", "oauth2" ], "contributors": [ - { "name": "Thom Seddon", "email": "thom@seddonmedia.co.uk" }, - { "name": "Lars F. Karlström" , "email": "lars@lfk.io" }, - { "name": "Rui Marinho", "email": "ruipmarinho@gmail.com" }, - { "name" : "Tiago Ribeiro", "email": "tiago.ribeiro@gmail.com" }, - { "name": "Michael Salinger", "email": "mjsalinger@gmail.com" }, - { "name": "Nuno Sousa" }, - { "name": "Max Truxa" } + { + "name": "Thom Seddon", + "email": "thom@seddonmedia.co.uk" + }, + { + "name": "Lars F. Karlström", + "email": "lars@lfk.io" + }, + { + "name": "Rui Marinho", + "email": "ruipmarinho@gmail.com" + }, + { + "name": "Tiago Ribeiro", + "email": "tiago.ribeiro@gmail.com" + }, + { + "name": "Michael Salinger", + "email": "mjsalinger@gmail.com" + }, + { + "name": "Nuno Sousa" + }, + { + "name": "Max Truxa" + }, + { + "name": "Jonathon Hill", + "email": "jhill9693@gmail.com" + }, + { + "name": "Marco Lüthy", + "email": "marco.luethy@gmail.com" + } ], - "main": "index.js", - "dependencies": { - "basic-auth": "1.1.0", - "bluebird": "3.5.0", - "lodash": "4.17.4", - "promisify-any": "2.0.1", - "statuses": "1.3.1", - "type-is": "1.6.15" - }, - "devDependencies": { - "jshint": "2.9.4", - "mocha": "3.3.0", - "should": "11.2.1", - "sinon": "2.3.2" - }, - "license": "MIT", + "main": "index.ts", "engines": { - "node": ">=4.0" + "node": ">=8.10" }, + "license": "MIT", "scripts": { - "pretest": "./node_modules/.bin/jshint --config ./.jshintrc lib test", - "test": "NODE_ENV=test ./node_modules/.bin/mocha 'test/**/*_test.js'", - "test-debug": "NODE_ENV=test ./node_modules/.bin/mocha --inspect --debug-brk 'test/**/*_test.js'" + "lint": "tslint -p tsconfig.build.json -c tslint.json", + "lint:all": "tslint -p tsconfig.json -c tslint.json", + "build:clean": "shx rm -rf ./dist", + "build": "npm-run-all build:clean build:test:*", + "build:test:src": "tsc -p tsconfig.json", + "build:test:package": "node ./scripts/build-prod.js", + "build:prod": "npm-run-all build:clean build:prod:*", + "build:prod:src": "tsc -p tsconfig.build.json", + "build:prod:package": "node ./scripts/build-prod.js", + "test": "nyc mocha 'test/**/*.spec.ts' --config 'test/.mocharc.jsonc'", + "test:clean": "shx rm -rf ./coverage && shx rm -rf ./.nyc_output" }, "repository": { - "type": "git", - "url": "https://github.com/oauthjs/node-oauth2-server.git" + "url": "https://github.com/oauthjs/node-oauth2-server.git", + "type": "git" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.1", + "@types/mocha": "^7.0.2", + "@types/sinon": "^9.0.0", + "@types/basic-auth": "^1.1.3", + "@types/node": "^11.15.3", + "@types/statuses": "^1.5.0", + "@types/type-is": "^1.6.3", + "mocha": "^7.1.1", + "npm-run-all": "^4.1.5", + "nyc": "^15.0.1", + "should": "^13.2.3", + "shx": "^0.3.2", + "sinon": "^9.0.2", + "ts-node": "^8.9.0", + "tslint": "^6.1.1", + "typescript": "^3.8.3" + }, + "dependencies": { + "basic-auth": "^2.0.1", + "statuses": "^1.5.0", + "tslib": "^1.10.0", + "type-is": "^1.6.18" } } diff --git a/scripts/build-prod.js b/scripts/build-prod.js new file mode 100644 index 000000000..3f2771837 --- /dev/null +++ b/scripts/build-prod.js @@ -0,0 +1,10 @@ +const fs = require('fs'); +const packageJson = require('../package.json'); + +delete packageJson.scripts; +delete packageJson.devDependencies; +packageJson.main = 'index.js'; +fs.writeFileSync( + __dirname + '/../dist/package.json', + JSON.stringify(packageJson, null, 2), +); diff --git a/test/.mocharc.jsonc b/test/.mocharc.jsonc new file mode 100644 index 000000000..a8ff95b9d --- /dev/null +++ b/test/.mocharc.jsonc @@ -0,0 +1,7 @@ +{ + "require": ["ts-node/register", "should", "test/assertions"], + "reporter": "spec", + "slow": 75, + "timeout": 2000, + "ui": "bdd" +} diff --git a/test/assertions.js b/test/assertions.js old mode 100644 new mode 100755 index 6846e3cdf..f057cdeb5 --- a/test/assertions.js +++ b/test/assertions.js @@ -1,17 +1,12 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var should = require('should'); - /** * SHA-1 assertion. */ -should.Assertion.add('sha1', function() { - this.params = { operator: 'to be a valid SHA-1 hash' }; - - this.obj.should.match(/^[a-f0-9]{40}$/i); -}, true); +should.Assertion.add( + 'sha1', + function() { + this.params = { operator: 'to be a valid SHA-1 hash' }; + this.obj.should.match(/^[a-f0-9]{40}$/i); + }, + true, +); diff --git a/test/integration/grant-types/abstract-grant-type.spec.ts b/test/integration/grant-types/abstract-grant-type.spec.ts new file mode 100755 index 000000000..2bde67443 --- /dev/null +++ b/test/integration/grant-types/abstract-grant-type.spec.ts @@ -0,0 +1,231 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { AbstractGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; +/** + * Test `AbstractGrantType` integration. + */ +describe('AbstractGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.accessTokenLifetime` is missing', () => { + try { + new AbstractGrantType(); + should.fail('no error was thrown', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', () => { + try { + new AbstractGrantType({ accessTokenLifetime: 123 }); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should set the `accessTokenLifetime`', () => { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + }); + + grantType.accessTokenLifetime.should.equal(123); + }); + + it('should set the `model`', () => { + const model = {}; + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.model.should.equal(model); + }); + + it('should set the `refreshTokenLifetime`', () => { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + grantType.refreshTokenLifetime.should.equal(456); + }); + }); + + describe('generateAccessToken()', () => { + it('should return an access token', async () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + try { + const data: any = await handler.generateAccessToken(); + data.should.be.a.sha1(); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const model = { + generateAccessToken() { + return Promise.resolve({}); + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateAccessToken().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + generateAccessToken() { + return {}; + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateAccessToken().should.be.an.instanceOf(Promise); + }); + }); + + describe('generateRefreshToken()', () => { + it('should return a refresh token', async () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + try { + const data: any = await handler.generateRefreshToken(); + data.should.be.a.sha1(); + } catch (error) { + should.fail('should.fail fail', error.message); + } + }); + + it('should support promises', () => { + const model = { + generateRefreshToken() { + return Promise.resolve({}); + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateRefreshToken().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + generateRefreshToken() { + return {}; + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateRefreshToken().should.be.an.instanceOf(Promise); + }); + }); + + describe('getAccessTokenExpiresAt()', () => { + it('should return a date', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe('getRefreshTokenExpiresAt()', () => { + it('should return a refresh token', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe('getScope()', () => { + it('should throw an error if `scope` is invalid', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getScope(request); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + } + }); + + it('should allow the `scope` to be `undefined`', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + should.not.exist(handler.getScope(request)); + }); + + it('should return the scope', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getScope(request).should.equal('foo'); + }); + }); +}); diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js deleted file mode 100644 index 6da489cd9..000000000 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ /dev/null @@ -1,174 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var should = require('should'); - -/** - * Test `AbstractGrantType` integration. - */ - -describe('AbstractGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.accessTokenLifetime` is missing', function() { - try { - new AbstractGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new AbstractGrantType({ accessTokenLifetime: 123 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should set the `accessTokenLifetime`', function() { - var grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {} }); - - grantType.accessTokenLifetime.should.equal(123); - }); - - it('should set the `model`', function() { - var model = {}; - var grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.model.should.equal(model); - }); - - it('should set the `refreshTokenLifetime`', function() { - var grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - grantType.refreshTokenLifetime.should.equal(456); - }); - }); - - describe('generateAccessToken()', function() { - it('should return an access token', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateAccessToken() - .then(function(data) { - data.should.be.a.sha1; - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - generateAccessToken: function() { - return Promise.resolve({}); - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - generateAccessToken: function() { - return {}; - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); - }); - }); - - describe('generateRefreshToken()', function() { - it('should return a refresh token', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateRefreshToken() - .then(function(data) { - data.should.be.a.sha1; - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - generateRefreshToken: function() { - return Promise.resolve({}); - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - generateRefreshToken: function() { - return {}; - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); - }); - }); - - describe('getAccessTokenExpiresAt()', function() { - it('should return a date', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe('getRefreshTokenExpiresAt()', function() { - it('should return a refresh token', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - var request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `scope`'); - } - }); - - it('should allow the `scope` to be `undefined`', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - should.not.exist(handler.getScope(request)); - }); - - it('should return the scope', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - handler.getScope(request).should.equal('foo'); - }); - }); -}); diff --git a/test/integration/grant-types/authorization-code-grant-type.spec.ts b/test/integration/grant-types/authorization-code-grant-type.spec.ts new file mode 100755 index 000000000..f052a7b34 --- /dev/null +++ b/test/integration/grant-types/authorization-code-grant-type.spec.ts @@ -0,0 +1,1033 @@ +import * as should from 'should'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../../../lib/errors'; +import { AuthorizationCodeGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `AuthorizationCodeGrantType` integration. + */ + +describe('AuthorizationCodeGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new AuthorizationCodeGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getAuthorizationCode()`', () => { + try { + new AuthorizationCodeGrantType({ + accessTokenLifetime: 3600, + model: {}, + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `revokeAuthorizationCode()`', () => { + try { + const model = { + getAuthorizationCode: () => {}, + }; + + new AuthorizationCodeGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `revokeAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + }; + + new AuthorizationCodeGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle(undefined, undefined); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is invalid', () => { + const client: any = {}; + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + }); + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', async () => { + const client: any = { id: 'foobar' }; + const token = {}; + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => { + return true; + }, + saveToken: () => { + return token; + }, + validateScope: () => { + return 'foo'; + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await grantType.handle(request, client); + data.should.equal(token); + } catch (e) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode: () => { + return Promise.resolve({ + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }); + }, + revokeAuthorizationCode: () => { + return true; + }, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => { + return true; + }, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const client: any = { id: 'foobar' }; + // const model = { + // getAuthorizationCode: (code, callback) => { + // callback(undefined, { + // authorizationCode: 12345, + // client: { id: 'foobar' }, + // expiresAt: new Date(new Date().getTime() * 2), + // user: {}, + // }); + // }, + // revokeAuthorizationCode: (code, callback) => { + // callback(undefined, { + // authorizationCode: 12345, + // client: { id: 'foobar' }, + // expiresAt: new Date(new Date().getTime() / 2), + // user: {}, + // }); + // }, + // saveToken: (tokenToSave, client, user, callback) => { + // callback(undefined, tokenToSave); + // }, + // }; + // const grantType = new AuthorizationCodeGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + // const request = new Request({ + // body: { code: 12345 }, + // headers: {}, + // method: "ANY", + // query: {}, + // }); + + // grantType.handle(request, client).should.be.an.instanceOf(Promise); + // }); + }); + + describe('getAuthorizationCode()', () => { + it('should throw an error if the request body does not contain `code`', async () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `code`'); + } + }); + + it('should throw an error if `code` is invalid', async () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code`'); + } + }); + + it('should throw an error if `authorizationCode` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + }); + }); + + it('should throw an error if `authorizationCode.client` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => { + return { authorizationCode: 12345 }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + }); + }); + + it('should throw an error if `authorizationCode.expiresAt` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => { + return { authorizationCode: 12345, client: {}, user: {} }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `expiresAt` must be a Date instance', + ); + }); + }); + + it('should throw an error if `authorizationCode.user` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(), + }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `user` object', + ); + }); + }); + + it('should throw an error if the client id does not match', () => { + const client: any = { id: 123 }; + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + expiresAt: new Date(), + client: { id: 456 }, + user: {}, + }; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + }); + }); + + it('should throw an error if the auth code is expired', () => { + const client: any = { id: 123 }; + const date = new Date(new Date().getTime() / 2); + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + client: { id: 123 }, + expiresAt: date, + user: {}, + }; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code has expired', + ); + }); + }); + + it('should throw an error if the `redirectUri` is invalid', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + redirectUri: 'foobar', + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return authorizationCode; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: `redirect_uri` is not a valid URI', + ); + }); + }); + + it('should return an auth code', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return authorizationCode; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(data => { + data.should.equal(authorizationCode); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return Promise.resolve(authorizationCode); + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return authorizationCode; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const authorizationCode = { + // authorizationCode: 12345, + // client: { id: 'foobar' }, + // expiresAt: new Date(new Date().getTime() * 2), + // user: {}, + // }; + // const client: any = { id: 'foobar' }; + // const model = { + // getAuthorizationCode(code, callback) { + // callback(undefined, authorizationCode); + // }, + // revokeAuthorizationCode() {}, + // saveToken() {}, + // }; + // const grantType = new AuthorizationCodeGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + // const request = new Request({ + // body: { code: 12345 }, + // headers: {}, + // method: "ANY", + // query: {}, + // }); + + // grantType + // .getAuthorizationCode(request, client) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('validateRedirectUri()', () => { + it('should throw an error if `redirectUri` is missing', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return authorizationCode; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should throw an error if `redirectUri` is invalid', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return true; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345, redirect_uri: 'http://bar.foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: `redirect_uri` is invalid'); + } + }); + }); + + describe('revokeAuthorizationCode()', () => { + it('should revoke the auth code', async () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return true; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.revokeAuthorizationCode(authorizationCode); + data.should.equal(authorizationCode); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should throw an error when the auth code is invalid', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return false; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + return grantType + .revokeAuthorizationCode(authorizationCode) + .then(data => { + data.should.equal(authorizationCode); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + }); + }); + + it('should support promises', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return Promise.resolve(true); + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return authorizationCode; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const authorizationCode = { + // authorizationCode: 12345, + // client: {}, + // expiresAt: new Date(new Date().getTime() / 2), + // user: {}, + // }; + // const model = { + // getAuthorizationCode() {}, + // revokeAuthorizationCode(code, callback) { + // callback(undefined, authorizationCode); + // }, + // saveToken() {}, + // }; + // const grantType = new AuthorizationCodeGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + + // grantType + // .revokeAuthorizationCode(authorizationCode) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token, ''); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token, '') + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token: any = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken() { + return token; + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token, '') + .should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const token = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken(tokenToSave, client, user, callback) { + callback(undefined, token); + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken({}, {}, token, '').should.be.an.instanceOf(Promise); + }); */ + }); +}); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js deleted file mode 100644 index 7f84e3443..000000000 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ /dev/null @@ -1,594 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var ServerError = require('../../../lib/errors/server-error'); -var should = require('should'); - -/** - * Test `AuthorizationCodeGrantType` integration. - */ - -describe('AuthorizationCodeGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new AuthorizationCodeGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getAuthorizationCode()`', function() { - try { - new AuthorizationCodeGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function() { - try { - var model = { - getAuthorizationCode: function() {} - }; - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {} - }; - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is invalid', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); - }); - - it('should throw an error if `client` is missing', function() { - - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request, null); - } - catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return Promise.resolve({ authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function(code, callback) { callback(null, { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, - revokeAuthorizationCode: function(code, callback) { callback(null, { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }); }, - saveToken: function(tokenToSave, client, user, callback) { callback(null, tokenToSave); } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getAuthorizationCode()', function() { - it('should throw an error if the request body does not contain `code`', function() { - var client = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.getAuthorizationCode(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `code`'); - } - }); - - it('should throw an error if `code` is invalid', function() { - var client = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getAuthorizationCode(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code`'); - } - }); - - it('should throw an error if `authorizationCode` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); - }); - - it('should throw an error if `authorizationCode.client` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345 }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); - }); - - it('should throw an error if `authorizationCode.expiresAt` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `expiresAt` must be a Date instance'); - }); - }); - - it('should throw an error if `authorizationCode.user` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); - }); - }); - - it('should throw an error if the client id does not match', function() { - var client = { id: 123 }; - var model = { - getAuthorizationCode: function() { - return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; - }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); - }); - - it('should throw an error if the auth code is expired', function() { - var client = { id: 123 }; - var date = new Date(new Date() / 2); - var model = { - getAuthorizationCode: function() { - return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; - }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code has expired'); - }); - }); - - it('should throw an error if the `redirectUri` is invalid', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); - }); - }); - - it('should return an auth code', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return Promise.resolve(authorizationCode); }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function(code, callback) { callback(null, authorizationCode); }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('validateRedirectUri()', function() { - it('should throw an error if `redirectUri` is missing', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - } - }); - - it('should throw an error if `redirectUri` is invalid', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345, redirect_uri: 'http://bar.foo' }, headers: {}, method: {}, query: {} }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is invalid'); - } - }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should revoke the auth code', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it('should throw an error when the auth code is invalid', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return false; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); - }); - - it('should support promises', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return Promise.resolve(true); }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function(code, callback) { callback(null, authorizationCode); }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/grant-types/client-credentials-grant-type.spec.ts b/test/integration/grant-types/client-credentials-grant-type.spec.ts new file mode 100755 index 000000000..d9f1e3a9a --- /dev/null +++ b/test/integration/grant-types/client-credentials-grant-type.spec.ts @@ -0,0 +1,382 @@ +import * as should from 'should'; +import { InvalidArgumentError, InvalidGrantError } from '../../../lib/errors'; +import { ClientCredentialsGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `ClientCredentialsGrantType` integration. + */ + +describe('ClientCredentialsGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new ClientCredentialsGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getUserFromClient()`', () => { + try { + new ClientCredentialsGrantType({ + accessTokenLifetime: 3600, + model: {}, + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getUserFromClient()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getUserFromClient() {}, + }; + + new ClientCredentialsGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getUserFromClient() {}, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getUserFromClient() {}, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const token = {}; + const model = { + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, {} as any) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const token = {}; + const model = { + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, {} as any).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token = {}; + const model = { + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, {} as any).should.be.an.instanceOf(Promise); + }); + }); + + describe('getUserFromClient()', () => { + it('should throw an error if `user` is missing', () => { + const model = { + getUserFromClient() {}, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getUserFromClient({} as any) + .then(() => { + should.fail('should.fail', ''); + }) + .catch((e: any) => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + }); + }); + + it('should return a user', async () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient() { + return user; + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await grantType.getUserFromClient({} as any); + data.should.equal(user); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient() { + return Promise.resolve(user); + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUserFromClient({} as any).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient() { + return user; + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUserFromClient({} as any).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient(userId, callback) { + callback(null, user); + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUserFromClient({}).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getUserFromClient() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getUserFromClient() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token) + .should.be.an.instanceOf(Promise); + }); + + /* it('should support non-promises', () => { + const token = {}; + const model = { + getUserFromClient() {}, + saveToken() { + return token; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const token = {}; + const model = { + getUserFromClient() {}, + saveToken(tokenToSave, client, user, callback) { + callback(null, token); + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); */ + }); +}); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js deleted file mode 100644 index 15ec9cc0e..000000000 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ /dev/null @@ -1,256 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var should = require('should'); - -/** - * Test `ClientCredentialsGrantType` integration. - */ - -describe('ClientCredentialsGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new ClientCredentialsGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getUserFromClient()`', function() { - try { - new ClientCredentialsGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getUserFromClient()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getUserFromClient: function() {} - }; - - new ClientCredentialsGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getUserFromClient: function() {}, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getUserFromClient: function() {}, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var token = {}; - var model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, {}) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe('getUserFromClient()', function() { - it('should throw an error if `user` is missing', function() { - var model = { - getUserFromClient: function() {}, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.getUserFromClient(request, {}) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - it('should return a user', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function() { return user; }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.getUserFromClient(request, {}) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function() { return Promise.resolve(user); }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function() {return user; }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function(userId, callback) { callback(null, user); }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function() { return token; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/grant-types/implicit-grant-type.spec.ts b/test/integration/grant-types/implicit-grant-type.spec.ts new file mode 100644 index 000000000..0af699fb1 --- /dev/null +++ b/test/integration/grant-types/implicit-grant-type.spec.ts @@ -0,0 +1,269 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { ImplicitGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `ImplicitGrantType` integration. + */ + +describe('ImplicitGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new ImplicitGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = {}; + + new ImplicitGrantType({ model, accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + + it('should throw an error if the `user` parameter is missing', () => { + try { + const model = { + saveToken() {}, + }; + + new ImplicitGrantType({ model, accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `user`'); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + try { + await grantType.handle(); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const client = { id: 'foobar' }; + const token = { accessToken: 'foobar-token' }; + const model = { + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(should.fail); + }); + + it('should support promises', () => { + const client = { id: 'foobar' }; + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client = { id: 'foobar' }; + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const client = { id: 'foobar' }; + // const model = { + // saveToken(tokenToSave, client, user, callback) { + // callback(null, tokenToSave); + // }, + // }; + // const grantType:any = new ImplicitGrantType({ + // accessTokenLifetime: 123, + // model, + // user: {}, + // }); + // const request = new Request({ + // body: { code: 12345 }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // grantType.handle(request, client).should.be.an.instanceOf(Promise); + // grantType.handle(request, client).then(data => { + // data.should.have.keys('accessToken', 'accessTokenExpiresAt'); + // data.accessToken.should.be.type('string'); + // }); + // }); + }); + + describe('saveToken()', () => { + it('should save the token', () => { + const token = {}; + const model = { + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + return grantType + .saveToken(token) + .then(data => { + data.should.equal(token); + }) + .catch(should.fail); + }); + + it('should support promises', () => { + const token = {}; + const model = { + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token = {}; + const model = { + saveToken() { + return token; + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const token = {}; + // const model = { + // saveToken(tokenToSave, client, user, callback) { + // callback(null, token); + // }, + // }; + // const grantType:any = new ImplicitGrantType({ + // accessTokenLifetime: 123, + // model, + // user: {}, + // }); + + // grantType.saveToken(token).should.be.an.instanceOf(Promise); + // }); + }); +}); diff --git a/test/integration/grant-types/password-grant-type.spec.ts b/test/integration/grant-types/password-grant-type.spec.ts new file mode 100755 index 000000000..a02b37729 --- /dev/null +++ b/test/integration/grant-types/password-grant-type.spec.ts @@ -0,0 +1,506 @@ +import * as should from 'should'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, +} from '../../../lib/errors'; +import { PasswordGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `PasswordGrantType` integration. + */ + +describe('PasswordGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new PasswordGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getUser()`', () => { + try { + new PasswordGrantType({ accessTokenLifetime: 3600, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getUser()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getUser: () => {}, + }; + + new PasswordGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getUser: () => {}, + saveToken: () => {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getUser: () => {}, + saveToken: () => {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle({}, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser: () => { + return {}; + }, + saveToken: () => { + return token; + }, + validateScope: () => { + return 'baz'; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar', scope: 'baz' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser() { + return {}; + }, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser() { + return {}; + }, + saveToken() { + return token; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser(username, password, callback) { + callback(null, {}); + }, + saveToken(tokenToSave, client, user, callback) { + callback(null, token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('getUser()', () => { + it('should throw an error if the request body does not contain `username`', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `username`'); + } + }); + + it('should throw an error if the request body does not contain `password`', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `password`'); + } + }); + + it('should throw an error if `username` is invalid', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: '\r\n', password: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `username`'); + } + }); + + it('should throw an error if `password` is invalid', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foobar', password: '\r\n' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `password`'); + } + }); + + it('should throw an error if `user` is missing', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await grantType.getUser(request); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + } + }); + + it('should return a user', async () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser() { + return user; + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await grantType.getUser(request); + data.should.equal(user); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser() { + return Promise.resolve(user); + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUser(request).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser() { + return user; + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUser(request).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser(username, password, callback) { + callback(null, user); + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUser(request).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getUser() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getUser() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token) + .should.be.an.instanceOf(Promise); + }); + + /* it('should support non-promises', () => { + const token = {}; + const model = { + getUser() {}, + saveToken() { + return token; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const token = {}; + const model = { + getUser() {}, + saveToken(tokenToSave, client, user, callback) { + callback(null, token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken({}, {}, token).should.be.an.instanceOf(Promise); + }); */ + }); +}); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js deleted file mode 100644 index 9cdf50dd5..000000000 --- a/test/integration/grant-types/password-grant-type_test.js +++ /dev/null @@ -1,344 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var should = require('should'); - -/** - * Test `PasswordGrantType` integration. - */ - -describe('PasswordGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new PasswordGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getUser()`', function() { - try { - new PasswordGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getUser()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getUser: function() {} - }; - - new PasswordGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - try { - grantType.handle({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function() { return {}; }, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function() { return {}; }, - saveToken: function() { return token; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function(username, password, callback) { callback(null, {}); }, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getUser()', function() { - it('should throw an error if the request body does not contain `username`', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `username`'); - } - }); - - it('should throw an error if the request body does not contain `password`', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `password`'); - } - }); - - it('should throw an error if `username` is invalid', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `username`'); - } - }); - - it('should throw an error if `password` is invalid', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `password`'); - } - }); - - it('should throw an error if `user` is missing', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - it('should return a user', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function() { return user; }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return grantType.getUser(request) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function() { return Promise.resolve(user); }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function() { return user; }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function(username, password, callback) { callback(null, user); }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function() { return token; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/grant-types/refresh-token-grant-type.spec.ts b/test/integration/grant-types/refresh-token-grant-type.spec.ts new file mode 100755 index 000000000..c37ecb9e0 --- /dev/null +++ b/test/integration/grant-types/refresh-token-grant-type.spec.ts @@ -0,0 +1,864 @@ +import * as should from 'should'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../../../lib/errors'; +import { RefreshTokenGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `RefreshTokenGrantType` integration. + */ + +describe('RefreshTokenGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new RefreshTokenGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getRefreshToken()`', () => { + try { + new RefreshTokenGrantType({ accessTokenLifetime: 3600, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + }); + + it('should throw an error if the model does not implement `revokeToken()`', () => { + try { + const model = { + getRefreshToken() {}, + }; + + new RefreshTokenGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getRefreshToken() {}, + revokeToken() {}, + }; + + new RefreshTokenGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getRefreshToken: () => {}, + revokeToken: () => {}, + saveToken: () => {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getRefreshToken: () => {}, + revokeToken: () => {}, + saveToken: () => {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken: () => { + return token; + }, + revokeToken: () => { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + }, + saveToken: () => { + return token; + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return Promise.resolve({ + accessToken: 'foo', + client: { id: 123 }, + user: {}, + }); + }, + revokeToken() { + return Promise.resolve({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }); + }, + saveToken() { + return Promise.resolve({ accessToken: 'foo', client: {}, user: {} }); + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + /* it('should support non-promises', () => { + const client : any= { id: 123 }; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: { id: 123 }, user: {} }; + }, + revokeToken() { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + saveToken() { + return { accessToken: 'foo', client: {}, user: {} }; + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const client : any= { id: 123 }; + const model = { + getRefreshToken(refreshToken, callback) { + callback(null, { accessToken: 'foo', client: { id: 123 }, user: {} }); + }, + revokeToken(refreshToken, callback) { + callback(null, { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }); + }, + saveToken(tokenToSave, client, user, callback) { + callback(null, { accessToken: 'foo', client: {}, user: {} }); + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('getRefreshToken()', () => { + it('should throw an error if the `refreshToken` parameter is missing from the request body', async () => { + const client: any = {}; + const model = { + getRefreshToken: () => {}, + revokeToken: () => {}, + saveToken: () => {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `refresh_token`'); + } + }); + + it('should throw an error if `refreshToken` is not found', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: '12345' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should throw an error if `refreshToken.client` is missing', () => { + const client: any = {}; + const model = { + getRefreshToken() { + return {}; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + }); + }); + + it('should throw an error if `refreshToken.user` is missing', () => { + const client: any = {}; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + }); + }); + + it('should throw an error if the client id does not match', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should throw an error if `refresh_token` contains invalid characters', async () => { + const client: any = {}; + const model = { + getRefreshToken() { + return { client: { id: 456 }, user: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `refresh_token`'); + } + }); + + it('should throw an error if `refresh_token` is missing', () => { + const client: any = {}; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should throw an error if `refresh_token` is expired', () => { + const client: any = { id: 123 }; + const date = new Date(new Date().getTime() / 2); + const model = { + getRefreshToken() { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: date, + user: {}, + }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token has expired'); + }); + }); + + it('should throw an error if `refreshTokenExpiresAt` is not a date value', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: 'stringValue', + user: {}, + }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + }); + }); + + it('should return a token', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken() { + return token; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken() { + return Promise.resolve(token); + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken() { + return token; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const client : any= { id: 123 }; + // const token : any= { accessToken: 'foo', client: { id: 123 }, user: {} }; + // const model = { + // getRefreshToken(refreshToken, callback) { + // callback(undefined, token); + // }, + // revokeToken() {}, + // saveToken() {}, + // }; + // const grantType = new RefreshTokenGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + // const request = new Request({ + // body: { refresh_token: 'foobar' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // grantType + // .getRefreshToken(request, client) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('revokeToken()', () => { + it('should throw an error if the `token` is invalid', () => { + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + grantType + .revokeToken({} as any) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should revoke the token', () => { + const token: any = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken() { + return token; + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + return grantType + .revokeToken(token) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const token: any = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken() { + return Promise.resolve(token); + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token: any = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken() { + return token; + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const token : any= { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken(refreshToken, callback) { + callback(undefined, token); + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken() { + return token; + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token) + .should.be.an.instanceOf(Promise); + }); + + // it('should support non-promises', () => { + // const token : any= {}; + // const model = { + // getRefreshToken() {}, + // revokeToken() {}, + // saveToken() { + // return token; + // }, + // }; + // const grantType = new RefreshTokenGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + + // grantType.saveToken(token).should.be.an.instanceOf(Promise); + // }); + + // it('should support callbacks', () => { + // const token : any= {}; + // const model = { + // getRefreshToken() {}, + // revokeToken() {}, + // saveToken(tokenToSave, client, user, callback) { + // callback(null, token); + // }, + // }; + // const grantType = new RefreshTokenGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + + // grantType.saveToken(token).should.be.an.instanceOf(Promise); + // }); + }); +}); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js deleted file mode 100644 index 925396afe..000000000 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ /dev/null @@ -1,536 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var Promise = require('bluebird'); -var RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); -var Request = require('../../../lib/request'); -var ServerError = require('../../../lib/errors/server-error'); -var should = require('should'); - -/** - * Test `RefreshTokenGrantType` integration. - */ - -describe('RefreshTokenGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new RefreshTokenGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getRefreshToken()`', function() { - try { - new RefreshTokenGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getRefreshToken()`'); - } - }); - - it('should throw an error if the model does not implement `revokeToken()`', function() { - try { - var model = { - getRefreshToken: function() {} - }; - - new RefreshTokenGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeToken()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {} - }; - - new RefreshTokenGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return token; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { return Promise.resolve({ accessToken: 'foo', client: { id: 123 }, user: {} }); }, - revokeToken: function() { return Promise.resolve({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }); }, - saveToken: function() { return Promise.resolve({ accessToken: 'foo', client: {}, user: {} }); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return { accessToken: 'foo', client: {}, user: {} }; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function(refreshToken, callback) { callback(null, { accessToken: 'foo', client: { id: 123 }, user: {} }); }, - revokeToken: function(refreshToken, callback) { callback(null, { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }); }, - saveToken: function(tokenToSave, client, user, callback) { callback(null,{ accessToken: 'foo', client: {}, user: {} }); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getRefreshToken()', function() { - it('should throw an error if the `refreshToken` parameter is missing from the request body', function() { - var client = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `refresh_token`'); - } - }); - - it('should throw an error if `refreshToken` is not found', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { return; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should throw an error if `refreshToken.client` is missing', function() { - var client = {}; - var model = { - getRefreshToken: function() { return {}; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); - }); - }); - - it('should throw an error if `refreshToken.user` is missing', function() { - var client = {}; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); - }); - }); - - it('should throw an error if the client id does not match', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 456 }, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should throw an error if `refresh_token` contains invalid characters', function() { - var client = {}; - var model = { - getRefreshToken: function() { - return { client: { id: 456 }, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `refresh_token`'); - } - }); - - it('should throw an error if `refresh_token` is missing', function() { - var client = {}; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 456 }, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should throw an error if `refresh_token` is expired', function() { - var client = { id: 123 }; - var date = new Date(new Date() / 2); - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token has expired'); - }); - }); - - it('should throw an error if `refreshTokenExpiresAt` is not a date value', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); - }); - }); - - it('should return a token', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return Promise.resolve(token); }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function(refreshToken, callback) { callback(null, token); }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('revokeToken()', function() { - it('should throw an error if the `token` is invalid', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - grantType.revokeToken({}) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should revoke the token', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function() { return token; }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function() { return Promise.resolve(token); }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function() { return token; }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function(refreshToken, callback) { callback(null, token); }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return token; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return token; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/handlers/authenticate-handler.spec.ts b/test/integration/handlers/authenticate-handler.spec.ts new file mode 100755 index 000000000..9200e9d01 --- /dev/null +++ b/test/integration/handlers/authenticate-handler.spec.ts @@ -0,0 +1,710 @@ +import * as should from 'should'; +import { + AccessDeniedError, + InsufficientScopeError, + InvalidArgumentError, + InvalidRequestError, + InvalidTokenError, + ServerError, + UnauthorizedRequestError, +} from '../../../lib/errors'; +import { AuthenticateHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; + +/** + * Test `AuthenticateHandler` integration. + */ + +describe('AuthenticateHandler integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.model` is missing', () => { + try { + new AuthenticateHandler(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getAccessToken()`', () => { + try { + new AuthenticateHandler({ model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + }); + + it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', () => { + try { + new AuthenticateHandler({ + model: { getAccessToken() {} }, + scope: 'foobar', + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `addAcceptedScopesHeader`'); + } + }); + + it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', () => { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + model: { getAccessToken() {} }, + scope: 'foobar', + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Missing parameter: `addAuthorizedScopesHeader`', + ); + } + }); + + it('should throw an error if `scope` was given and the model does not implement `verifyScope()`', () => { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: { getAccessToken() {} }, + scope: 'foobar', + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `verifyScope()`', + ); + } + }); + + it('should set the `model`', () => { + const model = { getAccessToken() {} }; + const grantType = new AuthenticateHandler({ model }); + + grantType.model.should.equal(model); + }); + + it('should set the `scope`', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const grantType = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foobar', + }); + + grantType.scope.should.equal('foobar'); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + try { + await handler.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', () => { + const model = { + getAccessToken() { + throw new UnauthorizedRequestError(undefined, undefined); + }, + }; + const handler = new AuthenticateHandler({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('WWW-Authenticate') + .should.equal('Bearer realm="Service"'); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getAccessToken() { + throw new AccessDeniedError('Cannot request this access token'); + }, + }; + const handler = new AuthenticateHandler({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Cannot request this access token'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', () => { + const model = { + getAccessToken() { + throw new Error('Unhandled exception'); + }, + }; + const handler = new AuthenticateHandler({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + }); + }); + + it('should return an access token', () => { + const accessToken: any = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = { + getAccessToken() { + return accessToken; + }, + verifyScope() { + return true; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(data => { + data.should.equal(accessToken); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('getTokenFromRequest()', () => { + it('should throw an error if more than one authentication method is used', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { access_token: 'foo' }, + }); + + try { + handler.getTokenFromRequest(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: only one authentication method is allowed', + ); + } + }); + + it('should throw an error if `accessToken` is missing', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequest(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedRequestError); + e.message.should.equal('Unauthorized request: no authentication given'); + } + }); + }); + + describe('getTokenFromRequestHeader()', () => { + it('should throw an error if the token is malformed', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'foobar', + }, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequestHeader(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: malformed authorization header', + ); + } + }); + + it('should return the bearer token', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: {}, + }); + + const bearerToken = handler.getTokenFromRequestHeader(request); + + bearerToken.should.equal('foo'); + }); + }); + + describe('getTokenFromRequestQuery()', () => { + it('should throw an error if the query contains a token', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + try { + handler.getTokenFromRequestQuery(undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: do not send bearer tokens in query URLs', + ); + } + }); + + it('should return the bearer token if `allowBearerTokensInQueryString` is true', () => { + const handler = new AuthenticateHandler({ + allowBearerTokensInQueryString: true, + model: { getAccessToken() {} }, + }); + const req = { query: { access_token: 'foo' } }; + handler.getTokenFromRequestQuery(req as Request).should.equal('foo'); + }); + }); + + describe('getTokenFromRequestBody()', () => { + it('should throw an error if the method is `GET`', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'GET', + query: {}, + }); + + try { + handler.getTokenFromRequestBody(request); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: token may not be passed in the body when using the GET verb', + ); + } + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequestBody(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + }); + + it('should return the bearer token', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + handler.getTokenFromRequestBody(request).should.equal('foo'); + }); + }); + + describe('getAccessToken()', () => { + it('should throw an error if `accessToken` is missing', () => { + const model = { + getAccessToken() {}, + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token is invalid'); + }); + }); + + it('should throw an error if `accessToken.user` is missing', () => { + const model = { + getAccessToken() { + return {}; + }, + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + }); + }); + + it('should return an access token', () => { + const accessToken: any = { user: {} }; + const model = { + getAccessToken() { + return accessToken; + }, + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(data => { + data.should.equal(accessToken); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const model = { + getAccessToken() { + return Promise.resolve({ user: {} }); + }, + }; + const handler = new AuthenticateHandler({ model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + getAccessToken() { + return { user: {} }; + }, + }; + const handler = new AuthenticateHandler({ model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const model = { + getAccessToken(token, callback) { + callback(null, { user: {} }); + }, + }; + const handler = new AuthenticateHandler({ model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); */ + }); + + describe('validateAccessToken()', () => { + it('should throw an error if `accessToken` is expired', () => { + const accessToken: any = { + accessTokenExpiresAt: new Date(new Date().getTime() / 2), + }; + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + try { + handler.validateAccessToken(accessToken); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token has expired'); + } + }); + + it('should return an access token', () => { + const accessToken: any = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + handler.validateAccessToken(accessToken).should.equal(accessToken); + }); + }); + + describe('verifyScope()', () => { + it('should throw an error if `scope` is insufficient', () => { + const model = { + getAccessToken() {}, + verifyScope() { + return false; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + return handler + .verifyScope('foo' as any) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InsufficientScopeError); + e.message.should.equal( + 'Insufficient scope: authorized scope is insufficient', + ); + }); + }); + + it('should support promises', () => { + const model = { + getAccessToken() {}, + verifyScope() { + return true; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + handler.verifyScope('foo' as any).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + getAccessToken() {}, + verifyScope() { + return true; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + handler.verifyScope('foo' as any).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const model = { + getAccessToken() {}, + verifyScope(token, scope, callback) { + callback(null, true); + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + handler.verifyScope('foo').should.be.an.instanceOf(Promise); + }); */ + }); + + describe('updateResponse()', () => { + it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.headers.should.not.have.property('x-accepted-oauth-scopes'); + }); + + it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model, + scope: 'foo bar', + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); + }); + + it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.headers.should.not.have.property('x-oauth-scopes'); + }); + + it('should set the `X-Authorized-OAuth-Scopes` header', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model, + scope: 'foo bar', + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.get('X-OAuth-Scopes').should.equal('foo biz'); + }); + }); +}); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js deleted file mode 100644 index 7852ea2eb..000000000 --- a/test/integration/handlers/authenticate-handler_test.js +++ /dev/null @@ -1,563 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AccessDeniedError = require('../../../lib/errors/access-denied-error'); -var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var InsufficientScopeError = require('../../../lib/errors/insufficient-scope-error'); -var InvalidTokenError = require('../../../lib/errors/invalid-token-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var ServerError = require('../../../lib/errors/server-error'); -var UnauthorizedRequestError = require('../../../lib/errors/unauthorized-request-error'); -var should = require('should'); - -/** - * Test `AuthenticateHandler` integration. - */ - -describe('AuthenticateHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.model` is missing', function() { - try { - new AuthenticateHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getAccessToken()`', function() { - try { - new AuthenticateHandler({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); - } - }); - - it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', function() { - try { - new AuthenticateHandler({ model: { getAccessToken: function() {} }, scope: 'foobar' }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `addAcceptedScopesHeader`'); - } - }); - - it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', function() { - try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, model: { getAccessToken: function() {} }, scope: 'foobar' }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `addAuthorizedScopesHeader`'); - } - }); - - 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' }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `verifyScope()`'); - } - }); - - it('should set the `model`', function() { - var model = { getAccessToken: function() {} }; - var grantType = new AuthenticateHandler({ model: model }); - - grantType.model.should.equal(model); - }); - - it('should set the `scope`', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var grantType = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: 'foobar' - }); - - grantType.scope.should.equal('foobar'); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', function() { - var model = { - getAccessToken: function() { - throw new UnauthorizedRequestError(); - } - }; - var handler = new AuthenticateHandler({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('WWW-Authenticate').should.equal('Bearer realm="Service"'); - }); - }); - - it('should throw the error if an oauth error is thrown', function() { - var model = { - getAccessToken: function() { - throw new AccessDeniedError('Cannot request this access token'); - } - }; - var handler = new AuthenticateHandler({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Cannot request this access token'); - }); - }); - - it('should throw a server error if a non-oauth error is thrown', function() { - var model = { - getAccessToken: function() { - throw new Error('Unhandled exception'); - } - }; - var handler = new AuthenticateHandler({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Unhandled exception'); - }); - }); - - it('should return an access token', function() { - var accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - var model = { - getAccessToken: function() { - return accessToken; - }, - verifyScope: function() { - return true; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - var request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - }); - - describe('getTokenFromRequest()', function() { - it('should throw an error if more than one authentication method is used', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: { access_token: 'foo' } - }); - - try { - handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: only one authentication method is allowed'); - } - }); - - it('should throw an error if `accessToken` is missing', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedRequestError); - e.message.should.equal('Unauthorized request: no authentication given'); - } - }); - }); - - describe('getTokenFromRequestHeader()', function() { - it('should throw an error if the token is malformed', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { - 'Authorization': 'foobar' - }, - method: {}, - query: {} - }); - - try { - handler.getTokenFromRequestHeader(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: malformed authorization header'); - } - }); - - it('should return the bearer token', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: {} - }); - - var bearerToken = handler.getTokenFromRequestHeader(request); - - bearerToken.should.equal('foo'); - }); - }); - - describe('getTokenFromRequestQuery()', function() { - it('should throw an error if the query contains a token', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - handler.getTokenFromRequestQuery(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: do not send bearer tokens in query URLs'); - } - }); - - it('should return the bearer token if `allowBearerTokensInQueryString` is true', function() { - var handler = new AuthenticateHandler({ allowBearerTokensInQueryString: true, model: { getAccessToken: function() {} } }); - - handler.getTokenFromRequestQuery({ query: { access_token: 'foo' } }).should.equal('foo'); - }); - }); - - describe('getTokenFromRequestBody()', function() { - it('should throw an error if the method is `GET`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: 'GET', - query: {} - }); - - try { - handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: token may not be passed in the body when using the GET verb'); - } - }); - - it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: {}, - query: {} - }); - - try { - handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); - } - }); - - it('should return the bearer token', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: {}, - query: {} - }); - - handler.getTokenFromRequestBody(request).should.equal('foo'); - }); - }); - - describe('getAccessToken()', function() { - it('should throw an error if `accessToken` is missing', function() { - var model = { - getAccessToken: function() {} - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token is invalid'); - }); - }); - - it('should throw an error if `accessToken.user` is missing', function() { - var model = { - getAccessToken: function() { - return {}; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAccessToken()` did not return a `user` object'); - }); - }); - - it('should return an access token', function() { - var accessToken = { user: {} }; - var model = { - getAccessToken: function() { - return accessToken; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - getAccessToken: function() { - return Promise.resolve({ user: {} }); - } - }; - var handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getAccessToken: function() { - return { user: {} }; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getAccessToken: function(token, callback) { - callback(null, { user: {} }); - } - }; - var handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - }); - - describe('validateAccessToken()', function() { - it('should throw an error if `accessToken` is expired', function() { - var accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - handler.validateAccessToken(accessToken); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token has expired'); - } - }); - - it('should return an access token', function() { - var accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - handler.validateAccessToken(accessToken).should.equal(accessToken); - }); - }); - - describe('verifyScope()', function() { - it('should throw an error if `scope` is insufficient', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() { - return false; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - return handler.verifyScope('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal('Insufficient scope: authorized scope is insufficient'); - }); - }); - - it('should support promises', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope('foo').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope('foo').should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function(token, scope, callback) { - callback(null, true); - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope('foo').should.be.an.instanceOf(Promise); - }); - }); - - describe('updateResponse()', function() { - it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.headers.should.not.have.property('x-accepted-oauth-scopes'); - }); - - it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: 'foo bar' }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); - }); - - it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.headers.should.not.have.property('x-oauth-scopes'); - }); - - it('should set the `X-Authorized-OAuth-Scopes` header', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: 'foo bar' }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.get('X-OAuth-Scopes').should.equal('foo biz'); - }); - }); -}); diff --git a/test/integration/handlers/authorize-handler.spec.ts b/test/integration/handlers/authorize-handler.spec.ts new file mode 100755 index 000000000..81eb04700 --- /dev/null +++ b/test/integration/handlers/authorize-handler.spec.ts @@ -0,0 +1,1622 @@ +import * as should from 'should'; +import * as url from 'url'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidScopeError, + ServerError, + UnauthorizedClientError, +} from '../../../lib/errors'; +import { AuthenticateHandler, AuthorizeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; +import { CodeResponseType } from '../../../lib/response-types'; + +/** + * Test `AuthorizeHandler` integration. + */ + +describe('AuthorizeHandler integration', () => { + describe('constructor()', () => { + // Move to Code Response Type + // it('should throw an error if `options.authorizationCodeLifetime` is missing', () => { + // try { + // new AuthorizeHandler({ model: {} }); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(InvalidArgumentError); + // e.message.should.equal( + // 'Missing parameter: `authorizationCodeLifetime`', + // ); + // } + // }); + + it('should throw an error if `options.model` is missing', () => { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', () => { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + // Move to Code Response Type + // it('should throw an error if the model does not implement `saveAuthorizationCode()`', () => { + // try { + // new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model: { getClient: () => {} }, + // }); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(InvalidArgumentError); + // e.message.should.equal( + // 'Invalid argument: model does not implement `saveAuthorizationCode()`', + // ); + // } + // }); + + it('should throw an error if the model does not implement `getAccessToken()`', () => { + const model = { + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + }); + + // it('should set the `authorizationCodeLifetime`', () => { + // const model = { + // getAccessToken: () => {}, + // getClient: () => {}, + // saveAuthorizationCode: () => {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler.authorizationCodeLifetime.should.equal(120); + // }); + + it('should set the `authenticateHandler`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); + }); + + it('should set the `model`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + try { + await handler.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if `allowed` is `false`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { allowed: 'false' }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal( + 'Access denied: user denied access to application', + ); + }); + }); + + it('should redirect to an error response if a non-oauth error is thrown', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new Error('Unhandled exception'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('location') + .should.equal( + 'http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar', + ); + }); + }); + + it('should redirect to an error response if an oauth error is thrown', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new AccessDeniedError('Cannot request this auth code'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.get('location').should.equal( + // tslint:disable-next-line:max-line-length + 'http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar', + ); + }); + }); + + it('should redirect to a successful response with `code` and `state` if successful', () => { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = { + getAccessToken: () => { + return { + client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return client; + }, + saveAuthorizationCode: () => { + return { authorizationCode: 12345, client }; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + response + .get('location') + .should.equal('http://example.com/cb?code=12345&state=foobar'); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should redirect to an error response if `scope` is invalid', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + return {}; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + scope: [], + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60', + ); + }); + }); + + it('should redirect to an error response if `state` is missing', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new AccessDeniedError('Cannot request this auth code'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60', + ); + }); + }); + + it('should redirect to an error response if `response_type` is invalid', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + return { authorizationCode: 12345, client: {} }; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'test', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.get('location').should.equal( + // tslint:disable-next-line:max-line-length + 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar', + ); + }); + }); + + it('should fail on invalid `response_type` before calling model.saveAuthorizationCode()', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new Error('must not be reached'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'test', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.get('location').should.equal( + // tslint:disable-next-line:max-line-length + 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar', + ); + }); + }); + + it('should return the `code` if successful', () => { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = { + getAccessToken: () => { + return { + client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() { + return client; + }, + saveAuthorizationCode() { + return { authorizationCode: 12345, client }; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(data => { + data.should.eql({ + authorizationCode: 12345, + client, + }); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + // describe('generateAuthorizationCode()', () => { + // it('should return an auth code', async () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // try { + // const data: any = await handler.generateAuthorizationCode( + // undefined, + // undefined, + // undefined, + // ); + // data.should.be.a.sha1(); + // } catch (error) { + // should.fail('should.fail', ''); + // } + // }); + + // it('should support promises', async () => { + // const model = { + // generateAuthorizationCode() { + // return Promise.resolve({}); + // }, + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // try { + // await handler + // .generateAuthorizationCode(undefined, undefined, undefined) + // .should.be.an.instanceOf(Promise); + // } catch (error) { + // should.fail('should.fail', ''); + // } + // }); + + // /* it('should support non-promises', () => { + // const model = { + // generateAuthorizationCode() { + // return {}; + // }, + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .generateAuthorizationCode(undefined, undefined, undefined) + // .should.be.an.instanceOf(Promise); + // }); */ + // }); + + // describe('getAuthorizationCodeLifetime()', () => { + // it('should return a date', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); + // }); + // }); + + describe('getClient()', () => { + it('should throw an error if `client_id` is missing', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `client_id`'); + } + }); + + it('should throw an error if `client_id` is invalid', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 'øå€£‰', response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `client.redirectUri` is invalid', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + redirect_uri: 'foobar', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should throw an error if `client` is missing', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: client credentials are invalid', + ); + }); + }); + + it('should throw an error if `client.grants` is missing', () => { + const model = { + getAccessToken() {}, + getClient() { + return {}; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: missing client `grants`'); + }); + }); + + it('should throw an error if `client` is unauthorized', () => { + const model = { + getAccessToken() {}, + getClient() { + return { grants: [] }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal( + 'Unauthorized client: `grant_type` is invalid', + ); + }); + }); + + it('should throw an error if `client.redirectUri` is missing', () => { + const model = { + getAccessToken() {}, + getClient() { + return { grants: ['authorization_code'] }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: missing client `redirectUri`', + ); + }); + }); + + it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', () => { + const model = { + getAccessToken() {}, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['https://example.com'], + }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + redirect_uri: 'https://foobar.com', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: `redirect_uri` does not match client value', + ); + }); + }); + + it('should support promises', async () => { + const model = { + getAccessToken() {}, + async getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + handler.getClient(request).should.be.an.instanceOf(Promise); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + /* it('should support non-promises', async () => { + const model = { + getAccessToken() {}, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + await handler.getClient(request).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const model = { + getAccessToken() {}, + getClient(clientId, clientSecret, callback) { + should.equal(clientSecret, null); + callback(null, { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }); + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); */ + + describe('with `client_id` in the request query', () => { + it('should return a client', () => { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = { + getAccessToken() {}, + getClient() { + return client; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: 'ANY', + query: { client_id: 12345 }, + }); + + return handler + .getClient(request) + .then(data => { + data.should.equal(client); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + }); + + describe('getScope()', () => { + it('should throw an error if `scope` is invalid', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getScope(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + } + }); + + describe('with `scope` in the request body', () => { + it('should return the scope', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getScope(request).should.equal('foo'); + }); + }); + + describe('with `scope` in the request query', () => { + it('should return the scope', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { scope: 'foo' }, + }); + + handler.getScope(request).should.equal('foo'); + }); + }); + }); + + describe('getState()', () => { + it('should throw an error if `allowEmptyState` is false and `state` is missing', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + allowEmptyState: false, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getState(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + } + }); + + it('should throw an error if `state` is invalid', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { state: 'øå€£‰' }, + }); + + try { + handler.getState(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `state`'); + } + }); + + describe('with `state` in the request body', () => { + it('should return the state', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { state: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getState(request).should.equal('foobar'); + }); + }); + + describe('with `state` in the request query', () => { + it('should return the state', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { state: 'foobar' }, + }); + + handler.getState(request).should.equal('foobar'); + }); + }); + }); + + describe('getUser()', () => { + it('should throw an error if `user` is missing', () => { + const authenticateHandler = { handle() {} }; + const model = { + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `handle()` did not return a `user` object', + ); + }); + }); + + it('should return a user', () => { + const user = {}; + const model = { + getAccessToken() { + return { + user, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getUser(request, response) + .then(data => { + data.should.equal(user); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + // describe('saveAuthorizationCode()', () => { + // it('should return an auth code', () => { + // const authorizationCode = {}; + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() { + // return authorizationCode; + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // return handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'biz', + // 'baz' as any, + // undefined, + // undefined, + // ) + // .then(data => { + // data.should.equal(authorizationCode); + // }) + // .catch(() => should.fail('should.fail', '')); + // }); + + // it('should support promises when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() { + // return Promise.resolve({}); + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'biz', + // 'baz' as any, + // undefined, + // undefined, + // ) + // .should.be.an.instanceOf(Promise); + // }); + + // /* it('should support non-promises when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() { + // return {}; + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode('foo', 'bar', 'biz', 'baz', undefined, undefined) + // .should.be.an.instanceOf(Promise); + // }); */ + + // /* it('should support callbacks when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode(code, client, user, callback) { + // return callback(null, true); + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + // .should.be.an.instanceOf(Promise); + // }); */ + // }); + + // describe('getResponseType()', () => { + // it('should throw an error if `response_type` is missing', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: {}, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // try { + // handler.getResponseType(request); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(InvalidRequestError); + // e.message.should.equal('Missing parameter: `response_type`'); + // } + // }); + + // it('should throw an error if `response_type` is not `code`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: { response_type: 'foobar' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // try { + // handler.getResponseType(request); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(UnsupportedResponseTypeError); + // e.message.should.equal( + // 'Unsupported response type: `response_type` is not supported', + // ); + // } + // }); + + // describe('with `response_type` in the request body', () => { + // it('should return a response type', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: { response_type: 'code' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + // const ResponseType = handler.getResponseType(request); + + // ResponseType.should.equal(CodeResponseType); + // }); + // }); + + // describe('with `response_type` in the request query', () => { + // it('should return a response type', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: {}, + // headers: {}, + // method: 'ANY', + // query: { response_type: 'code' }, + // }); + // const ResponseType = handler.getResponseType(request); + + // ResponseType.should.equal(CodeResponseType); + // }); + // }); + // }); + + describe('buildSuccessRedirectUri()', () => { + it('should return a redirect uri', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + responseType.code = 12345; + const redirectUri = handler.buildSuccessRedirectUri( + 'http://example.com/cb', + responseType, + ); + + url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); + }); + }); + + describe('buildErrorRedirectUri()', () => { + it('should set `error_description` if available', () => { + const error = new InvalidClientError('foo bar'); + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + const redirectUri = handler.buildErrorRedirectUri( + 'http://example.com/cb', + responseType, + error, + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?error=invalid_client&error_description=foo%20bar', + ); + }); + + it('should return a redirect uri', () => { + const error = new InvalidClientError(); + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + const redirectUri = handler.buildErrorRedirectUri( + 'http://example.com/cb', + responseType, + error, + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?error=invalid_client&error_description=Bad%20Request', + ); + }); + }); + + describe('updateResponse()', () => { + it('should set the `location` header', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + const response = new Response({ body: {}, headers: {} }); + const uri = url.parse('http://example.com/cb', true); + + handler.updateResponse(response, uri, responseType, 'foobar'); + + response + .get('location') + .should.equal('http://example.com/cb?state=foobar'); + }); + }); +}); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js deleted file mode 100644 index 0d1aa333b..000000000 --- a/test/integration/handlers/authorize-handler_test.js +++ /dev/null @@ -1,1126 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AccessDeniedError = require('../../../lib/errors/access-denied-error'); -var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); -var CodeResponseType = require('../../../lib/response-types/code-response-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidClientError = require('../../../lib/errors/invalid-client-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); -var UnsupportedResponseTypeError = require('../../../lib/errors/unsupported-response-type-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var ServerError = require('../../../lib/errors/server-error'); -var UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); -var should = require('should'); -var url = require('url'); - -/** - * Test `AuthorizeHandler` integration. - */ - -describe('AuthorizeHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.authorizationCodeLifetime` is missing', function() { - try { - new AuthorizeHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `authorizationCodeLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getClient()`', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getClient()`'); - } - }); - - it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: function() {} } }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `getAccessToken()`', function() { - var model = { - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); - } - }); - - it('should set the `authorizationCodeLifetime`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.authorizationCodeLifetime.should.equal(120); - }); - - it('should set the `authenticateHandler`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); - }); - - it('should set the `model`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.model.should.equal(model); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - try { - handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should throw an error if `response` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - }); - - it('should throw an error if `allowed` is `false`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { allowed: 'false' } }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Access denied: user denied access to application'); - }); - }); - - it('should redirect to an error response if a non-oauth error is thrown', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new Error('Unhandled exception'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); - }); - }); - - it('should redirect to an error response if an oauth error is thrown', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new AccessDeniedError('Cannot request this auth code'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); - }); - }); - - it('should redirect to a successful response with `code` and `state` if successful', function() { - var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - var model = { - getAccessToken: function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - response.get('location').should.equal('http://example.com/cb?code=12345&state=foobar'); - }) - .catch(should.fail); - }); - - it('should redirect to an error response if `scope` is invalid', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return {}; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - scope: [], - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60'); - }); - }); - - it('should redirect to an error response if `state` is missing', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new AccessDeniedError('Cannot request this auth code'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); - }); - }); - - it('should redirect to an error response if `response_type` is invalid', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: {} }; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'test' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); - }); - - it('should fail on invalid `response_type` before calling model.saveAuthorizationCode()', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new Error('must not be reached'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'test' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); - }); - - it('should return the `code` if successful', function() { - var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - var model = { - getAccessToken: function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.eql({ - authorizationCode: 12345, - client: client - }); - }) - .catch(should.fail); - }); - }); - - describe('generateAuthorizationCode()', function() { - it('should return an auth code', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.generateAuthorizationCode() - .then(function(data) { - data.should.be.a.sha1; - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - generateAuthorizationCode: function() { - return Promise.resolve({}); - }, - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - generateAuthorizationCode: function() { - return {}; - }, - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); - }); - }); - - describe('getAuthorizationCodeLifetime()', function() { - it('should return a date', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); - }); - }); - - describe('getClient()', function() { - it('should throw an error if `client_id` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `client_id`'); - } - }); - - it('should throw an error if `client_id` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 'øå€£‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - } - }); - - it('should throw an error if `client.redirectUri` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client credentials are invalid'); - }); - }); - - it('should throw an error if `client.grants` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return {}; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: missing client `grants`'); - }); - }); - - it('should throw an error if `client` is unauthorized', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return { grants: [] }; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - }); - }); - - it('should throw an error if `client.redirectUri` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { return { grants: ['authorization_code'] }; }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: missing client `redirectUri`'); - }); - }); - - it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['https://example.com'] }; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: `redirect_uri` does not match client value'); - }); - }); - - it('should support promises', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return Promise.resolve({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }); - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getAccessToken: function() {}, - getClient: function(clientId, clientSecret, callback) { - should.equal(clientSecret, null); - callback(null, { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }); - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - describe('with `client_id` in the request query', function() { - it('should return a client', function() { - var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - var model = { - getAccessToken: function() {}, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: { client_id: 12345 } }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid parameter: `scope`'); - } - }); - - describe('with `scope` in the request body', function() { - it('should return the scope', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - handler.getScope(request).should.equal('foo'); - }); - }); - - describe('with `scope` in the request query', function() { - it('should return the scope', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { scope: 'foo' } }); - - handler.getScope(request).should.equal('foo'); - }); - }); - }); - - describe('getState()', function() { - it('should throw an error if `allowEmptyState` is false and `state` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `state`'); - } - }); - - it('should throw an error if `state` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'øå€£‰' } }); - - try { - handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `state`'); - } - }); - - describe('with `state` in the request body', function() { - it('should return the state', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); - - handler.getState(request).should.equal('foobar'); - }); - }); - - describe('with `state` in the request query', function() { - it('should return the state', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); - - handler.getState(request).should.equal('foobar'); - }); - }); - }); - - describe('getUser()', function() { - it('should throw an error if `user` is missing', function() { - var authenticateHandler = { handle: function() {} }; - var model = { - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - var response = new Response(); - - return handler.getUser(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `handle()` did not return a `user` object'); - }); - }); - - it('should return a user', function() { - var user = {}; - var model = { - getAccessToken: function() { - return { - user: user, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.getUser(request, response) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - }); - - describe('saveAuthorizationCode()', function() { - it('should return an auth code', function() { - var authorizationCode = {}; - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() { - return authorizationCode; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz') - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it('should support promises when calling `model.saveAuthorizationCode()`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() { - return Promise.resolve({}); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises when calling `model.saveAuthorizationCode()`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() { - return {}; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); - }); - - it('should support callbacks when calling `model.saveAuthorizationCode()`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function(code, client, user, callback) { - return callback(null, true); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); - }); - }); - - describe('getResponseType()', function() { - it('should throw an error if `response_type` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `response_type`'); - } - }); - - it('should throw an error if `response_type` is not `code`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal('Unsupported response type: `response_type` is not supported'); - } - }); - - describe('with `response_type` in the request body', function() { - it('should return a response type', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - var ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - - describe('with `response_type` in the request query', function() { - it('should return a response type', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); - var ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - }); - - describe('buildSuccessRedirectUri()', function() { - it('should return a redirect uri', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var responseType = new CodeResponseType(12345); - var redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); - - url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); - }); - }); - - describe('buildErrorRedirectUri()', function() { - it('should set `error_description` if available', function() { - var error = new InvalidClientError('foo bar'); - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); - }); - - it('should return a redirect uri', function() { - var error = new InvalidClientError(); - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=Bad%20Request'); - }); - }); - - describe('updateResponse()', function() { - it('should set the `location` header', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var response = new Response({ body: {}, headers: {} }); - var uri = url.parse('http://example.com/cb'); - - handler.updateResponse(response, uri, 'foobar'); - - response.get('location').should.equal('http://example.com/cb?state=foobar'); - }); - }); -}); diff --git a/test/integration/handlers/revoke-handler.spec.ts b/test/integration/handlers/revoke-handler.spec.ts new file mode 100644 index 000000000..362802b9b --- /dev/null +++ b/test/integration/handlers/revoke-handler.spec.ts @@ -0,0 +1,1114 @@ +import * as should from 'should'; +import * as util from 'util'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidTokenError, + ServerError, +} from '../../../lib/errors'; +import { RevokeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; + +/** + * Test `RevokeHandler` integration. + */ + +describe('RevokeHandler integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.model` is missing', () => { + try { + new RevokeHandler({}); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', () => { + try { + new RevokeHandler({ model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + it('should set the `model`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + try { + await handler.handle(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handle(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if the method is not `POST`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'GET', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: method must be POST'); + }); + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'hash' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getClient() { + return { grants: ['password'] }; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it('should update the response if an error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(() => { + response.body.should.eql({ + error: 'server_error', + error_description: 'Unhandled exception', + }); + response.status.should.equal(500); + }); + }); + + it('should not update the response if an invalid token error is thrown', () => { + const token = { + refreshToken: 'hash', + client: {}, + user: {}, + refreshTokenExpiresAt: new Date('2015-01-01'), + }; + const client = { grants: ['password'] }; + const model = { + getClient() { + return client; + }, + revokeToken() { + return token; + }, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e[0].should.be.an.instanceOf(InvalidTokenError); + e[1].should.be.an.instanceOf(InvalidTokenError); + response.body.should.eql({}); + response.status.should.equal(200); + }); + }); + + it('should return an empty object if successful', () => { + const token = { + refreshToken: 'hash', + client: {}, + user: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + }; + const client = { grants: ['password'] }; + const model = { + getClient() { + return client; + }, + revokeToken() { + return token; + }, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(data => { + should.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', () => { + it('should throw an error if `clientId` is invalid', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'øå€£‰', client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `clientId` is invalid', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + } + }); + + it('should throw an error if `client` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should throw an error if `client.grants` is missing', () => { + const model = { + getClient() { + return {}; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: missing client `grants`'); + }); + }); + + it('should throw a 401 error if the client is invalid and the request contains an authorization header', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getClient(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal('Invalid client: client is invalid'); + + response + .get('WWW-Authenticate') + .should.equal('Basic realm="Service"'); + }); + }); + + it('should return a client', () => { + const client = { id: 12345, grants: [] }; + const model = { + getClient() { + return client; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(data => { + data.should.equal(client); + }) + .catch(should.fail); + }); + + it('should support promises', () => { + const model = { + getClient() { + return Promise.resolve({ grants: [] }); + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const model = { + // getClient(clientId, clientSecret, callback) { + // callback(null, { grants: [] }); + // }, + // revokeToken() {}, + // getRefreshToken() {}, + // getAccessToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + // const request = new Request({ + // body: { client_id: 12345, client_secret: 'secret' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // handler.getClient(request).should.be.an.instanceOf(Promise); + // }); + + it('should support non-promises', () => { + const model = { + getClient() { + return { grants: [] }; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + }); + + describe('getClientCredentials()', () => { + it('should throw an error if `client_id` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + it('should throw an error if `client_secret` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + describe('with `client_id` and `client_secret` in the request header as basic auth', () => { + it('should return a client', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request body', () => { + it('should return a client', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + }); + + describe('handleRevokeToken()', () => { + it('should throw an error if `token` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleRevokeToken(request) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should return a token', () => { + const client = { id: 12345, grants: ['password'] }; + const token = { + accessToken: 'hash', + client: { id: 12345 }, + accessTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }; + const model = { + getClient() {}, + revokeToken() { + return token; + }, + getRefreshToken() {}, + getAccessToken() { + return token; + }, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'hash' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleRevokeToken(request, client) + .then(data => { + should.exist(data); + }) + .catch(should.fail); + }); + + it('should return a token', () => { + const client = { id: 12345, grants: ['password'] }; + const token = { + refreshToken: 'hash', + client: { id: 12345 }, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }; + const model = { + getClient() {}, + revokeToken() { + return token; + }, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'hash' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleRevokeToken(request, client) + .then(data => { + should.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', () => { + it('should throw an error if the `refreshToken` is invalid', () => { + const client = {}; + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getRefreshToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: refresh token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', () => { + const client = { id: 'foo' }; + const token = { + refreshToken: 'hash', + client: { id: 'baz' }, + user: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getRefreshToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should return a token', () => { + const client = { id: 'foo' }; + const token = { + refreshToken: 'hash', + client: { id: 'foo' }, + user: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getRefreshToken('hash', client) + .then(Token => { + should.exist(Token); + }) + .catch(should.fail); + }); + + // it('should support callbacks', () => { + // const client = { id: 'foo' }; + // const token = { + // refreshToken: 'hash', + // client: { id: 'foo' }, + // user: {}, + // refreshTokenExpiresAt: new Date(Date.now() * 2), + // }; + // const model = { + // getClient() {}, + // revokeToken() {}, + // getRefreshToken(refreshToken, callback) { + // callback(null, token); + // }, + // getAccessToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + + // return handler + // .getRefreshToken('hash', client) + // .then(token => { + // should.exist(token); + // }) + // .catch(should.fail); + // }); + }); + + describe('getAccessToken()', () => { + it('should throw an error if the `accessToken` is invalid', () => { + const client = {}; + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() {}, + getRefreshToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getAccessToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', () => { + const client = { id: 'foo' }; + const token = { + accessToken: 'hash', + client: { id: 'baz' }, + user: {}, + accessTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() { + return token; + }, + getRefreshToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getAccessToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should return a token', () => { + const client = { id: 'foo' }; + const token = { + accessToken: 'hash', + client: { id: 'foo' }, + user: {}, + accessTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() { + return token; + }, + getRefreshToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getAccessToken('hash', client) + .then(Token => { + should.exist(Token); + }) + .catch(should.fail); + }); + + // it('should support callbacks', () => { + // const client = { id: 'foo' }; + // const token = { + // accessToken: 'hash', + // client: { id: 'foo' }, + // user: {}, + // accessTokenExpiresAt: new Date(Date.now() * 2), + // }; + // const model = { + // getClient() {}, + // revokeToken() {}, + // getAccessToken(accessToken, callback) { + // callback(null, token); + // }, + // getRefreshToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + + // return handler + // .getAccessToken('hash', client) + // .then(token => { + // should.exist(token); + // }) + // .catch(should.fail); + // }); + }); + + describe('revokeToken()', () => { + it('should throw an error if the `refreshToken` is invalid', () => { + const token = 'hash'; + const client = {}; + const model = { + getClient() {}, + revokeToken() { + return false; + }, + getRefreshToken() { + return { client: {}, user: {} }; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .revokeToken(token, client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: token is invalid'); + }); + }); + + // it('should support callbacks', () => { + // const token = {}; + // const client = {}; + // const model = { + // getClient() {}, + // revokeToken(tokenObject, callback) { + // callback(null, null); + // }, + // getRefreshToken(refreshToken, callback) { + // callback(null, { client: {}, user: {} }); + // }, + // getAccessToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + + // return handler + // .revokeToken(token, client) + // .then(should.fail) + // .catch(e => { + // e.should.be.an.instanceOf(InvalidTokenError); + // e.message.should.equal('Invalid token: token is invalid'); + // }); + // }); + }); + + describe('getTokenFromRequest()', () => { + it('should throw an error if `accessToken` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequest(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + } + }); + }); + + describe('updateErrorResponse()', () => { + it('should set the `body`', () => { + const error = new AccessDeniedError('Cannot request a revoke'); + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a revoke'); + }); + + it('should set the `status`', () => { + const error = new AccessDeniedError('Cannot request a revoke'); + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); +}); diff --git a/test/integration/handlers/token-handler.spec.ts b/test/integration/handlers/token-handler.spec.ts new file mode 100755 index 000000000..25e861479 --- /dev/null +++ b/test/integration/handlers/token-handler.spec.ts @@ -0,0 +1,1670 @@ +import * as should from 'should'; +import * as util from 'util'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + ServerError, + UnauthorizedClientError, + UnsupportedGrantTypeError, +} from '../../../lib/errors'; +import { PasswordGrantType } from '../../../lib/grant-types'; +import { TokenHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; +import { BearerTokenType } from '../../../lib/token-types'; + +/** + * Test `TokenHandler` integration. + */ + +describe('TokenHandler integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.accessTokenLifetime` is missing', () => { + try { + new TokenHandler(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', () => { + try { + new TokenHandler({ accessTokenLifetime: 120 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if `options.refreshTokenLifetime` is missing', () => { + try { + new TokenHandler({ accessTokenLifetime: 120, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `refreshTokenLifetime`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', () => { + try { + new TokenHandler({ + accessTokenLifetime: 120, + model: {}, + refreshTokenLifetime: 120, + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + it('should set the `accessTokenLifetime`', () => { + const accessTokenLifetime = {}; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime, + model, + refreshTokenLifetime: 120, + }); + + handler.accessTokenLifetime.should.equal(accessTokenLifetime); + }); + + it('should set the `alwaysIssueNewRefreshToken`', () => { + const alwaysIssueNewRefreshToken = true; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it('should set the `alwaysIssueNewRefreshToken` to false', () => { + const alwaysIssueNewRefreshToken = false; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it('should return the default `alwaysIssueNewRefreshToken` value', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 120, + }); + + handler.alwaysIssueNewRefreshToken.should.equal(true); + }); + + it('should set the `extendedGrantTypes`', () => { + const extendedGrantTypes = { foo: 'bar' }; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + extendedGrantTypes, + model, + refreshTokenLifetime: 120, + }); + + handler.grantTypes.should.containEql(extendedGrantTypes); + }); + + it('should set the `model`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.model.should.equal(model); + }); + + it('should set the `refreshTokenLifetime`', () => { + const refreshTokenLifetime = {}; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime, + }); + + handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + try { + await handler.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if the method is not `POST`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'GET', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: method must be POST'); + }); + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + getUser() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it('should update the response if an error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + getUser() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.body.should.eql({ + error: 'server_error', + error_description: 'Unhandled exception', + }); + response.status.should.equal(500); + }); + }); + + it('should return a bearer token if successful', async () => { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: 'foobar', + user: {}, + }; + const model = { + getClient() { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + try { + const data = await handler.handle(request, response); + data.should.eql(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', () => { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: 'foobar', + user: {}, + foo: 'bar', + }; + const model = { + getClient() { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + should.exist(response.body.scope); + should.not.exist(response.body.foo); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', async () => { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: 'foobar', + user: {}, + foo: 'bar', + }; + const model = { + getClient() { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + allowExtendedTokenAttributes: true, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + await handler.handle(request, response); + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + should.exist(response.body.scope); + should.exist(response.body.foo); + }); + }); + + describe('getClient()', () => { + it('should throw an error if `clientId` is invalid', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'øå€£‰', client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `clientSecret` is invalid', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + } + }); + + it('should throw an error if `client` is missing', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should throw an error if `client.grants` is missing', () => { + const model = { + getClient() { + return {}; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: missing client `grants`'); + }); + }); + + it('should throw an error if `client.grants` is invalid', async () => { + const model = { + getClient() { + return { grants: 'foobar' }; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await handler.getClient(request, undefined); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `grants` must be an array'); + } + }); + + it('should throw a 401 error if the client is invalid and the request contains an authorization header', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getClient(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal('Invalid client: client is invalid'); + + response + .get('WWW-Authenticate') + .should.equal('Basic realm="Service"'); + }); + }); + + it('should return a client', async () => { + const client = { id: 12345, grants: [] }; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await handler.getClient(request, undefined); + data.should.equal(client); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + describe('with `password` grant type and `requireClientAuthentication` is false', () => { + it('should return a client ', () => { + const client = { id: 12345, grants: [] }; + const model = { + async getClient() { + return client; + }, + saveToken() {}, + }; + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { client_id: 'blah', grant_type: 'password' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(data => { + data.should.equal(client); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with `password` grant type and `requireClientAuthentication` is false and Authorization header', () => { + it('should return a client ', () => { + const client = { id: 12345, grants: [] }; + const model = { + async getClient() { + return client; + }, + saveToken() {}, + }; + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { grant_type: 'password' }, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('blah:').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(data => { + data.should.equal(client); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + it('should support promises', () => { + const model = { + getClient() { + return Promise.resolve({ grants: [] }); + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request, undefined).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + getClient() { + return { grants: [] }; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request, undefined).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const model = { + getClient(clientId, clientSecret, callback) { + callback(null, { grants: [] }); + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request, undefined).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('getClientCredentials()', () => { + it('should throw an error if `client_id` is missing', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + it('should throw an error if `client_secret` is missing', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', () => { + it('should return a client', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + requireClientAuthentication: { password: false }, + }); + const request = new Request({ + body: { client_id: 'foo', grant_type: 'password' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request header as basic auth', () => { + it('should return a client', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request body', () => { + it('should return a client', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + }); + + describe('handleGrantType()', () => { + it('should throw an error if `grant_type` is missing', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `grant_type`'); + } + }); + + it('should throw an error if `grant_type` is invalid', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: '~foo~' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, undefined); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `grant_type`'); + } + }); + + it('should throw an error if `grant_type` is unsupported', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedGrantTypeError); + e.message.should.equal( + 'Unsupported grant type: `grant_type` is invalid', + ); + } + }); + + it('should throw an error if `grant_type` is unauthorized', async () => { + const client: any = { grants: ['client_credentials'] }; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'password' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, client); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal('Unauthorized client: `grant_type` is invalid'); + } + }); + + /* it('should throw an invalid grant error if a non-oauth error is thrown', () => { + const client = { grants: ['password'] }; + const model = { + getClient(clientId, password, callback) { + callback(null, client); + }, + getUser(uid, pwd, callback) { + callback(); + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'password', username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(() => should.fail('should.fail', '')) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + }); + }); */ + + describe('with grant_type `authorization_code`', () => { + it('should return a token', () => { + const client: any = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + getClient() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + revokeAuthorizationCode() { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler.handleGrantType(request, client).then(data => { + data.should.equal(token); + }); + // .catch(() => { + // should.fail('should.fail', ''); + // }); + }); + }); + + describe('with grant_type `client_credentials`', () => { + it('should return a token', () => { + const client: any = { grants: ['client_credentials'] }; + const token = {}; + const model = { + getClient() {}, + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: 'client_credentials', + scope: 'foo', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with grant_type `password`', () => { + it('should return a token', () => { + const client: any = { grants: ['password'] }; + const token = {}; + const model = { + getClient() {}, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + scope: 'baz', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with grant_type `refresh_token`', () => { + it('should return a token', () => { + const client: any = { grants: ['refresh_token'] }; + const token = { accessToken: 'foo', client: {}, user: {} }; + const model = { + getClient() {}, + getRefreshToken() { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + saveToken() { + return token; + }, + revokeToken() { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: 'refresh_token', + refresh_token: 12345, + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with custom grant_type', () => { + it('should return a token', () => { + const client: any = { + grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'], + }; + const token = {}; + const model = { + getClient() {}, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + 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: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + }); + + describe('getAccessTokenLifetime()', () => { + it('should return the client access token lifetime', () => { + const client: any = { accessTokenLifetime: 60 }; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(60); + }); + + it('should return the default access token lifetime', () => { + const client: any = {}; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(120); + }); + }); + + describe('getRefreshTokenLifetime()', () => { + it('should return the client access token lifetime', () => { + const client: any = { refreshTokenLifetime: 60 }; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(60); + }); + + it('should return the default access token lifetime', () => { + const client: any = {}; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(120); + }); + }); + + describe('getTokenType()', () => { + it('should return a token type', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = handler.getTokenType({ + accessToken: 'foo', + refreshToken: 'bar', + scope: 'foobar', + }); + + tokenType.should.containEql({ + accessToken: 'foo', + accessTokenLifetime: undefined, + refreshToken: 'bar', + scope: 'foobar', + }); + }); + }); + + describe('updateSuccessResponse()', () => { + it('should set the `body`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.body.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + + it('should set the `Cache-Control` header', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get('Cache-Control').should.equal('no-store'); + }); + + it('should set the `Pragma` header', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get('Pragma').should.equal('no-cache'); + }); + }); + + describe('updateErrorResponse()', () => { + it('should set the `body`', () => { + const error = new AccessDeniedError('Cannot request a token'); + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a token'); + }); + + it('should set the `status`', () => { + const error = new AccessDeniedError('Cannot request a token'); + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); +}); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js deleted file mode 100644 index 50277c113..000000000 --- a/test/integration/handlers/token-handler_test.js +++ /dev/null @@ -1,1079 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AccessDeniedError = require('../../../lib/errors/access-denied-error'); -var BearerTokenType = require('../../../lib/token-types/bearer-token-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidClientError = require('../../../lib/errors/invalid-client-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var ServerError = require('../../../lib/errors/server-error'); -var TokenHandler = require('../../../lib/handlers/token-handler'); -var UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); -var UnsupportedGrantTypeError = require('../../../lib/errors/unsupported-grant-type-error'); -var should = require('should'); -var util = require('util'); - -/** - * Test `TokenHandler` integration. - */ - -describe('TokenHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.accessTokenLifetime` is missing', function() { - try { - new TokenHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new TokenHandler({ accessTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if `options.refreshTokenLifetime` is missing', function() { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `refreshTokenLifetime`'); - } - }); - - it('should throw an error if the model does not implement `getClient()`', function() { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {}, refreshTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getClient()`'); - } - }); - - it('should set the `accessTokenLifetime`', function() { - var accessTokenLifetime = {}; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: accessTokenLifetime, model: model, refreshTokenLifetime: 120 }); - - handler.accessTokenLifetime.should.equal(accessTokenLifetime); - }); - - it('should set the `alwaysIssueNewRefreshToken`', function() { - var alwaysIssueNewRefreshToken = true; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); - - handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); - }); - - it('should set the `alwaysIssueNewRefreshToken` to false', function() { - var alwaysIssueNewRefreshToken = false; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); - - handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); - }); - - it('should return the default `alwaysIssueNewRefreshToken` value', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120 }); - - handler.alwaysIssueNewRefreshToken.should.equal(true); - }); - - it('should set the `extendedGrantTypes`', function() { - var extendedGrantTypes = { foo: 'bar' }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, extendedGrantTypes: extendedGrantTypes, model: model, refreshTokenLifetime: 120 }); - - handler.grantTypes.should.containEql(extendedGrantTypes); - }); - - it('should set the `model`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.model.should.equal(model); - }); - - it('should set the `refreshTokenLifetime`', function() { - var refreshTokenLifetime = {}; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: refreshTokenLifetime }); - - handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - try { - handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should throw an error if `response` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - }); - - it('should throw an error if the method is not `POST`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: 'GET', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: method must be POST'); - }); - }); - - it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); - }); - }); - - it('should throw the error if an oauth error is thrown', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - }); - }); - - it('should throw a server error if a non-oauth error is thrown', function() { - var model = { - getClient: function() { - throw new Error('Unhandled exception'); - }, - getUser: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Unhandled exception'); - e.inner.should.be.an.instanceOf(Error); - }); - }); - - it('should update the response if an error is thrown', function() { - var model = { - getClient: function() { - throw new Error('Unhandled exception'); - }, - getUser: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); - response.status.should.equal(503); - }); - }); - - it('should return a bearer token if successful', function() { - var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {} }; - var model = { - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.eql(token); - }) - .catch(should.fail); - }); - - it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', function() { - var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {}, foo: 'bar' }; - var model = { - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - should.exist(response.body.scope); - should.not.exist(response.body.foo); - }) - .catch(should.fail); - }); - - it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', function() { - var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {}, foo: 'bar' }; - var model = { - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, allowExtendedTokenAttributes: true }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - should.exist(response.body.scope); - should.exist(response.body.foo); - }) - .catch(should.fail); - }); - }); - - - describe('getClient()', function() { - it('should throw an error if `clientId` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - } - }); - - it('should throw an error if `clientSecret` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_secret`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client is invalid'); - }); - }); - - it('should throw an error if `client.grants` is missing', function() { - var model = { - getClient: function() { return {}; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: missing client `grants`'); - }); - }); - - it('should throw an error if `client.grants` is invalid', function() { - var model = { - getClient: function() { return { grants: 'foobar' }; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `grants` must be an array'); - }); - }); - - it('should throw a 401 error if the client is invalid and the request contains an authorization header', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: {}, - headers: { 'authorization': util.format('Basic %s', new Buffer('foo:bar').toString('base64')) }, - method: {}, - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.getClient(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.code.should.equal(401); - e.message.should.equal('Invalid client: client is invalid'); - - response.get('WWW-Authenticate').should.equal('Basic realm="Service"'); - }); - }); - - it('should return a client', function() { - var client = { id: 12345, grants: [] }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - - describe('with `password` grant type and `requireClientAuthentication` is false', function() { - - it('should return a client ', function() { - var client = { id: 12345, grants: [] }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - - var handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false - } - }); - var request = new Request({ body: { client_id: 'blah', grant_type: 'password'}, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - describe('with `password` grant type and `requireClientAuthentication` is false and Authorization header', function() { - - it('should return a client ', function() { - var client = { id: 12345, grants: [] }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - - var handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false - } - }); - var request = new Request({ - body: { grant_type: 'password'}, - headers: { 'authorization': util.format('Basic %s', new Buffer('blah:').toString('base64')) }, - method: {}, - query: {} - }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - it('should support promises', function() { - var model = { - getClient: function() { return Promise.resolve({ grants: [] }); }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getClient: function() { return { grants: [] }; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getClient: function(clientId, clientSecret, callback) { callback(null, { grants: [] }); }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - }); - - describe('getClientCredentials()', function() { - it('should throw an error if `client_id` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - } - }); - - it('should throw an error if `client_secret` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - } - }); - - describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', function() { - it('should return a client', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, requireClientAuthentication: { password: false} }); - var request = new Request({ body: { client_id: 'foo', grant_type: 'password' }, headers: {}, method: {}, query: {} }); - var credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo' }); - }); - }); - - describe('with `client_id` and `client_secret` in the request header as basic auth', function() { - it('should return a client', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: {}, - headers: { - 'authorization': util.format('Basic %s', new Buffer('foo:bar').toString('base64')) - }, - method: {}, - query: {} - }); - var credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); - }); - }); - - describe('with `client_id` and `client_secret` in the request body', function() { - it('should return a client', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, query: {} }); - var credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); - }); - }); - }); - - describe('handleGrantType()', function() { - it('should throw an error if `grant_type` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `grant_type`'); - } - }); - - it('should throw an error if `grant_type` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: '~foo~' }, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `grant_type`'); - } - }); - - it('should throw an error if `grant_type` is unsupported', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedGrantTypeError); - e.message.should.equal('Unsupported grant type: `grant_type` is invalid'); - } - }); - - it('should throw an error if `grant_type` is unauthorized', function() { - var client = { grants: ['client_credentials'] }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - } - }); - - it('should throw an invalid grant error if a non-oauth error is thrown', function() { - var client = { grants: ['password'] }; - var model = { - getClient: function(clientId, password, callback) { callback(null, client); }, - getUser: function(uid, pwd, callback) { callback(); }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'password', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - describe('with grant_type `authorization_code`', function() { - it('should return a token', function() { - var client = { id: 'foobar', grants: ['authorization_code'] }; - var token = {}; - var model = { - 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'; }, - revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `client_credentials`', function() { - it('should return a token', function() { - var client = { grants: ['client_credentials'] }; - var token = {}; - var model = { - getClient: function() {}, - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - grant_type: 'client_credentials', - scope: 'foo' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `password`', function() { - it('should return a token', function() { - var client = { grants: ['password'] }; - var token = {}; - var model = { - getClient: function() {}, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo', - scope: 'baz' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `refresh_token`', function() { - it('should return a token', function() { - var client = { grants: ['refresh_token'] }; - var token = { accessToken: 'foo', client: {}, user: {} }; - var model = { - getClient: function() {}, - getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; }, - saveToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - grant_type: 'refresh_token', - refresh_token: 12345 - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with custom grant_type', function() { - it('should return a token', function() { - var client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; - var token = {}; - var model = { - getClient: function() {}, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, extendedGrantTypes: { 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType } }); - var request = new Request({ body: { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - }); - - describe('getAccessTokenLifetime()', function() { - it('should return the client access token lifetime', function() { - var client = { accessTokenLifetime: 60 }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getAccessTokenLifetime(client).should.equal(60); - }); - - it('should return the default access token lifetime', function() { - var client = {}; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getAccessTokenLifetime(client).should.equal(120); - }); - }); - - describe('getRefreshTokenLifetime()', function() { - it('should return the client access token lifetime', function() { - var client = { refreshTokenLifetime: 60 }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getRefreshTokenLifetime(client).should.equal(60); - }); - - it('should return the default access token lifetime', function() { - var client = {}; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getRefreshTokenLifetime(client).should.equal(120); - }); - }); - - describe('getTokenType()', function() { - it('should return a token type', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); - - tokenType.should.containEql({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: 'foobar' }); - }); - }); - - describe('updateSuccessResponse()', function() { - it('should set the `body`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = new BearerTokenType('foo', 'bar', 'biz'); - var response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.body.should.eql({ access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', token_type: 'Bearer' }); - }); - - it('should set the `Cache-Control` header', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = new BearerTokenType('foo', 'bar', 'biz'); - var response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get('Cache-Control').should.equal('no-store'); - }); - - it('should set the `Pragma` header', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = new BearerTokenType('foo', 'bar', 'biz'); - var response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get('Pragma').should.equal('no-cache'); - }); - }); - - describe('updateErrorResponse()', function() { - it('should set the `body`', function() { - var error = new AccessDeniedError('Cannot request a token'); - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.body.error.should.equal('access_denied'); - response.body.error_description.should.equal('Cannot request a token'); - }); - - it('should set the `status`', function() { - var error = new AccessDeniedError('Cannot request a token'); - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.status.should.equal(400); - }); - }); -}); diff --git a/test/integration/request.spec.ts b/test/integration/request.spec.ts new file mode 100755 index 000000000..9515ed7b3 --- /dev/null +++ b/test/integration/request.spec.ts @@ -0,0 +1,183 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../lib/errors'; +import { Request } from '../../lib/request'; + +/** + * Test `Request` integration. + */ + +describe('Request integration', () => { + describe('constructor()', () => { + it('should throw an error if `headers` is missing', () => { + try { + new Request({ body: {} } as any); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `headers`'); + } + }); + + it('should throw an error if `method` is missing', () => { + try { + new Request({ body: {}, headers: {} } as any); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `method`'); + } + }); + + it('should throw an error if `query` is missing', () => { + try { + new Request({ body: {}, headers: {}, method: 'ANY' } as any); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `query`'); + } + }); + + it('should set the `body`', () => { + const request = new Request({ + body: 'foo', + headers: {}, + method: 'ANY', + query: {}, + }); + + request.body.should.equal('foo'); + }); + + it('should set the `headers`', () => { + const request = new Request({ + body: {}, + headers: { foo: 'bar', QuX: 'biz' }, + method: 'ANY', + query: {}, + }); + + request.headers.should.eql({ foo: 'bar', qux: 'biz' }); + }); + + it('should set the `method`', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'biz', + query: {}, + }); + + request.method.should.equal('BIZ'); + }); + + it('should set the `query`', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: 'baz', + }); + + request.query.should.equal('baz'); + }); + }); + + describe('get()', () => { + it('should return `undefined` if the field does not exist', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + (request.get('content-type') === undefined).should.be.true(); + }); + + it('should return the value if the field exists', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + }, + method: 'ANY', + query: {}, + }); + + request.get('Content-Type').should.equal('text/html; charset=utf-8'); + }); + }); + + describe('is()', () => { + it('should accept an array of `types`', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is(['html', 'json']).should.equal('json'); + }); + + it('should accept multiple `types` as arguments', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is('html', 'json').should.equal('json'); + }); + + it('should return the first matching type', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is('html').should.equal('html'); + }); + + it('should return `false` if none of the `types` match', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is('json').should.be.false(); + }); + + it('should return `false` if the request has no body', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + request.is('text/html').should.be.false(); + }); + }); +}); diff --git a/test/integration/request_test.js b/test/integration/request_test.js deleted file mode 100644 index a43527671..000000000 --- a/test/integration/request_test.js +++ /dev/null @@ -1,159 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Request = require('../../lib/request'); -var InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -var should = require('should'); - -/** - * Test `Request` integration. - */ - -describe('Request integration', function() { - describe('constructor()', function() { - it('should throw an error if `headers` is missing', function() { - try { - new Request({ body: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `headers`'); - } - }); - - it('should throw an error if `method` is missing', function() { - try { - new Request({ body: {}, headers: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `method`'); - } - }); - - it('should throw an error if `query` is missing', function() { - try { - new Request({ body: {}, headers: {}, method: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `query`'); - } - }); - - it('should set the `body`', function() { - var request = new Request({ body: 'foo', headers: {}, method: {}, query: {} }); - - request.body.should.equal('foo'); - }); - - it('should set the `headers`', function() { - var request = new Request({ body: {}, headers: { foo: 'bar', QuX: 'biz' }, method: {}, query: {} }); - - request.headers.should.eql({ foo: 'bar', qux: 'biz' }); - }); - - it('should set the `method`', function() { - var request = new Request({ body: {}, headers: {}, method: 'biz', query: {} }); - - request.method.should.equal('biz'); - }); - - it('should set the `query`', function() { - var request = new Request({ body: {}, headers: {}, method: {}, query: 'baz' }); - - request.query.should.equal('baz'); - }); - }); - - describe('get()', function() { - it('should return `undefined` if the field does not exist', function() { - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - (undefined === request.get('content-type')).should.be.true; - }); - - it('should return the value if the field exists', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8' - }, - method: {}, - query: {} - }); - - request.get('Content-Type').should.equal('text/html; charset=utf-8'); - }); - }); - - describe('is()', function() { - it('should accept an array of `types`', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'application/json', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is(['html', 'json']).should.equal('json'); - }); - - it('should accept multiple `types` as arguments', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'application/json', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('html', 'json').should.equal('json'); - }); - - it('should return the first matching type', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('html').should.equal('html'); - }); - - it('should return `false` if none of the `types` match', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('json').should.be.false; - }); - - it('should return `false` if the request has no body', function() { - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - request.is('text/html').should.be.false; - }); - }); -}); diff --git a/test/integration/response-types/code-response-type.spec.ts b/test/integration/response-types/code-response-type.spec.ts new file mode 100755 index 000000000..1824a7f51 --- /dev/null +++ b/test/integration/response-types/code-response-type.spec.ts @@ -0,0 +1,339 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import * as url from 'url'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { CodeResponseType } from '../../../lib/response-types'; + +/** + * Test `CodeResponseType` integration. + */ + +describe('CodeResponseType integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.authorizationCodeLifetime` is missing', () => { + try { + new CodeResponseType(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Missing parameter: `authorizationCodeLifetime`', + ); + } + }); + + it('should set the `code`', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + responseType.authorizationCodeLifetime.should.equal(120); + }); + }); + + it('should throw an error if the model does not implement `saveAuthorizationCode()`', () => { + try { + new CodeResponseType({ authorizationCodeLifetime: 120, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveAuthorizationCode()`', + ); + } + }); + + it('should set the `authorizationCodeLifetime`', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler.authorizationCodeLifetime.should.equal(120); + }); + + describe('buildRedirectUri()', () => { + it('should throw an error if the `redirectUri` is missing', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + try { + responseType.buildRedirectUri(undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `redirectUri`'); + } + }); + + it('should return the new redirect uri and set the `code` and `state` in the query', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + responseType.code = 'foo'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb'), + ); + + url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); + }); + + it('should return the new redirect uri and append the `code` and `state` in the query', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + responseType.code = 'foo'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb?foo=bar', true), + ); + + url + .format(redirectUri) + .should.equal('http://example.com/cb?foo=bar&code=foo'); + }); + }); + + it('should set the `model`', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler.model.should.equal(model); + }); + + describe('generateAuthorizationCode()', () => { + it('should return an auth code', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .generateAuthorizationCode(undefined, undefined, undefined) + .then((data: any) => { + data.should.be.a.sha1(); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const model = { + generateAuthorizationCode: () => { + return Promise.resolve({}); + }, + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .generateAuthorizationCode(undefined, undefined, undefined) + .should.be.an.instanceOf(Promise); + }); + + // it('should support non-promises', () => { + // const model = { + // generateAuthorizationCode: () => { + // return {}; + // }, + // getAccessToken: () => {}, + // getClient: () => {}, + // saveAuthorizationCode: () => {}, + // }; + // const handler = new CodeResponseType({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .generateAuthorizationCode(undefined, undefined, undefined) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('getAuthorizationCodeExpiresAt()', () => { + it('should return a date', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler: any = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler.getAuthorizationCodeExpiresAt({}).should.be.an.instanceOf(Date); + }); + }); + + describe('saveAuthorizationCode()', () => { + it('should return an auth code', () => { + const authorizationCode = {}; + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => { + return authorizationCode; + }, + }; + const handler: any = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + .then(data => { + data.should.equal(authorizationCode); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises when calling `model.saveAuthorizationCode()`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => { + return Promise.resolve({}); + }, + }; + const handler: any = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz', undefined, undefined) + .should.be.an.instanceOf(Promise); + }); + + // it('should support non-promises when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken: () => {}, + // getClient: () => {}, + // saveAuthorizationCode: () => { + // return {}; + // }, + // }; + // const handler = new CodeResponseType({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'biz', + // 'baz' as any, + // undefined, + // undefined, + // ) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('saveAuthorizationCode()', () => { + it('should call `model.saveAuthorizationCode()`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: sinon.stub().returns({}), + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .saveAuthorizationCode( + 'foo', + 'bar' as any, + 'qux', + 'biz' as any, + 'baz', + 'boz' as any, + ) + .then(() => { + 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[1].should.equal('biz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('generateAuthorizationCode()', () => { + it('should call `model.generateAuthorizationCode()`', () => { + const model = { + generateAuthorizationCode: sinon.stub().returns({}), + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .generateAuthorizationCode(undefined, undefined, undefined) + .then(() => { + model.generateAuthorizationCode.callCount.should.equal(1); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js deleted file mode 100644 index 5461b62c4..000000000 --- a/test/integration/response-types/code-response-type_test.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var CodeResponseType = require('../../../lib/response-types/code-response-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var should = require('should'); -var url = require('url'); - -/** - * Test `CodeResponseType` integration. - */ - -describe('CodeResponseType integration', function() { - describe('constructor()', function() { - it('should throw an error if `code` is missing', function() { - try { - new CodeResponseType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `code`'); - } - }); - - it('should set the `code`', function() { - var responseType = new CodeResponseType('foo'); - - responseType.code.should.equal('foo'); - }); - }); - - describe('buildRedirectUri()', function() { - it('should throw an error if the `redirectUri` is missing', function() { - var responseType = new CodeResponseType('foo'); - - try { - responseType.buildRedirectUri(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `redirectUri`'); - } - }); - - it('should return the new redirect uri and set the `code` and `state` in the query', function() { - var responseType = new CodeResponseType('foo'); - var redirectUri = responseType.buildRedirectUri('http://example.com/cb'); - - url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); - }); - - it('should return the new redirect uri and append the `code` and `state` in the query', function() { - var responseType = new CodeResponseType('foo'); - var redirectUri = responseType.buildRedirectUri('http://example.com/cb?foo=bar'); - - url.format(redirectUri).should.equal('http://example.com/cb?foo=bar&code=foo'); - }); - }); -}); diff --git a/test/integration/response-types/token-response-type.spec.ts b/test/integration/response-types/token-response-type.spec.ts new file mode 100644 index 000000000..97847c8a6 --- /dev/null +++ b/test/integration/response-types/token-response-type.spec.ts @@ -0,0 +1,96 @@ +import * as should from 'should'; +import * as url from 'url'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { TokenResponseType } from '../../../lib/response-types'; + +/** + * Test `TokenResponseType` integration. + */ + +describe('TokenResponseType integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.accessTokenLifetime` is missing', () => { + try { + new TokenResponseType(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should set `accessTokenLifetime`', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + responseType.accessTokenLifetime.should.equal(120); + }); + + it('should set the `model`', () => { + const model = { + foobar() {}, + }; + const handler = new TokenResponseType({ + accessTokenLifetime: 120, + model, + }); + + handler.model.should.equal(model); + }); + }); + + describe('buildRedirectUri()', () => { + it('should throw an error if the `redirectUri` is missing', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + try { + responseType.buildRedirectUri(undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `redirectUri`'); + } + }); + + it('should return the new redirect uri and set `access_token` and `state` in the query', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + responseType.accessToken = 'foobar-token'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb'), + ); + + url + .format(redirectUri) + .should.equal('http://example.com/cb#access_token=foobar-token'); + }); + + it('should return the new redirect uri and append `access_token` and `state` in the query', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + responseType.accessToken = 'foobar-token'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb?foo=bar', true), + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?foo=bar#access_token=foobar-token', + ); + }); + }); +}); diff --git a/test/integration/response.spec.ts b/test/integration/response.spec.ts new file mode 100755 index 000000000..dc73475af --- /dev/null +++ b/test/integration/response.spec.ts @@ -0,0 +1,75 @@ +import { Response } from '../../lib/response'; + +/** + * Test `Response` integration. + */ + +describe('Response integration', () => { + describe('constructor()', () => { + it('should set the `body`', () => { + const response = new Response({ body: 'foo', headers: {} }); + + response.body.should.equal('foo'); + }); + + it('should set the `headers`', () => { + const response = new Response({ + body: {}, + headers: { foo: 'bar', QuX: 'biz' }, + }); + + response.headers.should.eql({ foo: 'bar', qux: 'biz' }); + }); + + it('should set the `status` to 200', () => { + const response = new Response({ body: {}, headers: {} }); + + response.status.should.equal(200); + }); + }); + + describe('get()', () => { + it('should return `undefined` if the field does not exist', () => { + const response = new Response({ body: {}, headers: {} }); + + (response.get('content-type') === undefined).should.be.true(); + }); + + it('should return the value if the field exists', () => { + const response = new Response({ + body: {}, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); + + response.get('Content-Type').should.equal('text/html; charset=utf-8'); + }); + }); + + describe('redirect()', () => { + it('should set the location header to `url`', () => { + const response = new Response({ body: {}, headers: {} }); + + response.redirect('http://example.com'); + + response.get('Location').should.equal('http://example.com'); + }); + + it('should set the `status` to 302', () => { + const response = new Response({ body: {}, headers: {} }); + + response.redirect('http://example.com'); + + response.status.should.equal(302); + }); + }); + + describe('set()', () => { + it('should set the `field`', () => { + const response = new Response({ body: {}, headers: {} }); + + response.set('foo', 'bar'); + + response.headers.should.eql({ foo: 'bar' }); + }); + }); +}); diff --git a/test/integration/response_test.js b/test/integration/response_test.js deleted file mode 100644 index 1e1e0206a..000000000 --- a/test/integration/response_test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Response = require('../../lib/response'); - -/** - * Test `Response` integration. - */ - -describe('Response integration', function() { - describe('constructor()', function() { - it('should set the `body`', function() { - var response = new Response({ body: 'foo', headers: {} }); - - response.body.should.equal('foo'); - }); - - it('should set the `headers`', function() { - var response = new Response({ body: {}, headers: { foo: 'bar', QuX: 'biz' } }); - - response.headers.should.eql({ foo: 'bar', qux: 'biz' }); - }); - - it('should set the `status` to 200', function() { - var response = new Response({ body: {}, headers: {} }); - - response.status.should.equal(200); - }); - }); - - describe('get()', function() { - it('should return `undefined` if the field does not exist', function() { - var response = new Response({ body: {}, headers: {} }); - - (undefined === response.get('content-type')).should.be.true; - }); - - it('should return the value if the field exists', function() { - var response = new Response({ body: {}, headers: { 'content-type': 'text/html; charset=utf-8' } }); - - response.get('Content-Type').should.equal('text/html; charset=utf-8'); - }); - }); - - describe('redirect()', function() { - it('should set the location header to `url`', function() { - var response = new Response({ body: {}, headers: {} }); - - response.redirect('http://example.com'); - - response.get('Location').should.equal('http://example.com'); - }); - - it('should set the `status` to 302', function() { - var response = new Response({ body: {}, headers: {} }); - - response.redirect('http://example.com'); - - response.status.should.equal(302); - }); - }); - - describe('set()', function() { - it('should set the `field`', function() { - var response = new Response({ body: {}, headers: {} }); - - response.set('foo', 'bar'); - - response.headers.should.eql({ foo: 'bar' }); - }); - }); -}); diff --git a/test/integration/server.spec.ts b/test/integration/server.spec.ts new file mode 100755 index 000000000..cda191909 --- /dev/null +++ b/test/integration/server.spec.ts @@ -0,0 +1,354 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { InvalidArgumentError } from '../../lib/errors'; +import { + AuthenticateHandler, + AuthorizeHandler, + TokenHandler, +} from '../../lib/handlers'; +import { Request } from '../../lib/request'; +import { Response } from '../../lib/response'; +import { OAuth2Server as Server } from '../../lib/server'; + +/** + * Test `Server` integration. + */ + +describe('Server integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new Server({}); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should set the `model`', () => { + const model = {}; + const server = new Server({ model }); + + server.options.model.should.equal(model); + }); + }); + + describe('authenticate()', () => { + it('should set the default `options`', async () => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + try { + const stub = sinon + .stub(AuthenticateHandler.prototype, 'handle') + .returnsThis(); + const token = await server.authenticate(request, response); + token.addAcceptedScopesHeader.should.be.true(); + token.addAuthorizedScopesHeader.should.be.true(); + token.allowBearerTokensInQueryString.should.be.false(); + stub.restore(); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should return a promise', () => { + const model = { + async getAccessToken(token) { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.authenticate(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', next => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + // server.authenticate(request, response, null, next); + }); */ + }); + + describe('authorize()', () => { + it('should set the default `options`', async () => { + const model = { + async getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + async getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + async saveAuthorizationCode() { + return { authorizationCode: 123 }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { state: 'foobar' }, + }); + const response = new Response({ body: {}, headers: {} }); + // try { + const stub = sinon + .stub(AuthorizeHandler.prototype, 'handle') + .returnsThis(); + const code = await server.authorize(request, response); + const options = code.options; + options.allowEmptyState.should.be.false(); + options.authorizationCodeLifetime.should.be.equal(300); + stub.restore(); + // } catch (error) { + // should.fail('should.fail', ''); + // } + }); + + it('should return a promise', () => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() { + return { authorizationCode: 123 }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { state: 'foobar' }, + }); + + const response = new Response({ body: {}, headers: {} }); + try { + const handler = server.authorize(request, response); + handler.should.be.an.instanceOf(Promise); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + /* it('should support callbacks', next => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() { + return { authorizationCode: 123 }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { state: 'foobar' }, + }); + const response = new Response({ body: {}, headers: {} }); + + // tslint:disable-next-line: no-floating-promises + // server.authorize(request, response, undefined, next); + }); */ + }); + + describe('token()', () => { + it('should set the default `options`', async () => { + const model = { + async getClient() { + return { grants: ['password'] }; + }, + async getUser() { + return {}; + }, + async saveToken() { + return { accessToken: 1234, client: {}, user: {} }; + }, + async validateScope() { + return 'foo'; + }, + }; + const server = new Server({ 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: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const stub = sinon.stub(TokenHandler.prototype, 'handle').returnsThis(); + // try { + const token = await server.token(request, response); + token.accessTokenLifetime.should.equal(3600); + token.refreshTokenLifetime.should.equal(1209600); + stub.restore(); + // } catch (error) { + // should.fail('should.fail', ''); + // } + }); + + it('should return a promise', () => { + const model = { + async getClient() { + return { grants: ['password'] }; + }, + async getUser() { + return {}; + }, + async saveToken() { + return { accessToken: 1234, client: {}, user: {} }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + grant_type: 'password', + username: 'foo', + password: 'pass', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + // try { + const handler = server.token(request, response); + + handler.should.be.an.instanceOf(Promise); + // } catch (error) { + // should.fail('should.fail', ''); + // } + }); + + /* it('should support callbacks', next => { + const model = { + async () { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return { accessToken: 1234, client: {}, user: {} }; + }, + validateScope() { + return 'foo'; + }, + }; + const server = new Server({ 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: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + // server.token(request, response, null, next); + }); */ + }); +}); diff --git a/test/integration/server_test.js b/test/integration/server_test.js deleted file mode 100644 index 2d3aa7845..000000000 --- a/test/integration/server_test.js +++ /dev/null @@ -1,238 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -var Promise = require('bluebird'); -var Request = require('../../lib/request'); -var Response = require('../../lib/response'); -var Server = require('../../lib/server'); -var should = require('should'); - -/** - * Test `Server` integration. - */ - -describe('Server integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new Server({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should set the `model`', function() { - var model = {}; - var server = new Server({ model: model }); - - server.options.model.should.equal(model); - }); - }); - - describe('authenticate()', function() { - it('should set the default `options`', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return server.authenticate(request, response) - .then(function() { - this.addAcceptedScopesHeader.should.be.true; - this.addAuthorizedScopesHeader.should.be.true; - this.allowBearerTokensInQueryString.should.be.false; - }) - .catch(should.fail); - }); - - it('should return a promise', function() { - var model = { - getAccessToken: function(token, callback) { - callback(null, { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }); - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - var handler = server.authenticate(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function(next) { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - server.authenticate(request, response, null, next); - }); - }); - - describe('authorize()', function() { - it('should set the default `options`', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - var response = new Response({ body: {}, headers: {} }); - - return server.authorize(request, response) - .then(function() { - this.allowEmptyState.should.be.false; - this.authorizationCodeLifetime.should.equal(300); - }) - .catch(should.fail); - }); - - it('should return a promise', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - var response = new Response({ body: {}, headers: {} }); - var handler = server.authorize(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function(next) { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - var response = new Response({ body: {}, headers: {} }); - - server.authorize(request, response, null, next); - }); - }); - - describe('token()', function() { - it('should set the default `options`', function() { - var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - }, - validateScope: function() { return 'foo'; } - }; - var server = new Server({ model: model }); - var 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: {} }); - var response = new Response({ body: {}, headers: {} }); - - return server.token(request, response) - .then(function() { - this.accessTokenLifetime.should.equal(3600); - this.refreshTokenLifetime.should.equal(1209600); - }) - .catch(should.fail); - }); - - it('should return a promise', function() { - var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - var handler = server.token(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function(next) { - var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - }, - validateScope: function() { - return 'foo'; - } - }; - var server = new Server({ model: model }); - var 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: {} }); - var response = new Response({ body: {}, headers: {} }); - - server.token(request, response, null, next); - }); - }); -}); diff --git a/test/integration/token-types/bearer-token-type.spec.ts b/test/integration/token-types/bearer-token-type.spec.ts new file mode 100755 index 000000000..44ef8ef11 --- /dev/null +++ b/test/integration/token-types/bearer-token-type.spec.ts @@ -0,0 +1,135 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { BearerTokenType } from '../../../lib/token-types'; + +/** + * Test `BearerTokenType` integration. + */ + +describe('BearerTokenType integration', () => { + describe('constructor()', () => { + it('should throw an error if `accessToken` is missing', () => { + try { + new BearerTokenType( + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + + it('should set the `accessToken`', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + undefined, + undefined, + undefined, + ); + + responseType.accessToken.should.equal('foo'); + }); + + it('should set the `accessTokenLifetime`', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + undefined, + undefined, + undefined, + ); + + responseType.accessTokenLifetime.should.equal('bar'); + }); + + it('should set the `refreshToken`', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + + responseType.refreshToken.should.equal('biz'); + }); + }); + + describe('valueOf()', () => { + it('should return the value representation', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + undefined, + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + token_type: 'Bearer', + }); + }); + + it('should not include the `expires_in` if not given', () => { + const responseType = new BearerTokenType( + 'foo', + undefined, + undefined, + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + token_type: 'Bearer', + }); + }); + + it('should set `refresh_token` if `refreshToken` is defined', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + + it('should set `expires_in` if `accessTokenLifetime` is defined', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + }); +}); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js deleted file mode 100644 index 3c1ef6bdd..000000000 --- a/test/integration/token-types/bearer-token-type_test.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var BearerTokenType = require('../../../lib/token-types/bearer-token-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var should = require('should'); - -/** - * Test `BearerTokenType` integration. - */ - -describe('BearerTokenType integration', function() { - describe('constructor()', function() { - it('should throw an error if `accessToken` is missing', function() { - try { - new BearerTokenType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessToken`'); - } - }); - - it('should set the `accessToken`', function() { - var responseType = new BearerTokenType('foo', 'bar'); - - responseType.accessToken.should.equal('foo'); - }); - - it('should set the `accessTokenLifetime`', function() { - var responseType = new BearerTokenType('foo', 'bar'); - - responseType.accessTokenLifetime.should.equal('bar'); - }); - - it('should set the `refreshToken`', function() { - var responseType = new BearerTokenType('foo', 'bar', 'biz'); - - responseType.refreshToken.should.equal('biz'); - }); - }); - - describe('valueOf()', function() { - it('should return the value representation', function() { - var responseType = new BearerTokenType('foo', 'bar'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - token_type: 'Bearer' - }); - }); - - it('should not include the `expires_in` if not given', function() { - var responseType = new BearerTokenType('foo'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - token_type: 'Bearer' - }); - }); - - it('should set `refresh_token` if `refreshToken` is defined', function() { - var responseType = new BearerTokenType('foo', 'bar', 'biz'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - refresh_token: 'biz', - token_type: 'Bearer' - }); - }); - - it('should set `expires_in` if `accessTokenLifetime` is defined', function() { - var responseType = new BearerTokenType('foo', 'bar', 'biz'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - refresh_token: 'biz', - token_type: 'Bearer' - }); - }); - }); -}); diff --git a/test/integration/utils/token-util.spec.ts b/test/integration/utils/token-util.spec.ts new file mode 100755 index 000000000..9a7ea1747 --- /dev/null +++ b/test/integration/utils/token-util.spec.ts @@ -0,0 +1,19 @@ +import * as should from 'should'; +import * as TokenUtil from '../../../lib/utils/token-util'; + +/** + * Test `TokenUtil` integration. + */ + +describe('TokenUtil integration', () => { + describe('generateRandomToken()', () => { + it('should return a sha-1 token', async () => { + try { + const token: any = await TokenUtil.GenerateRandomToken(); + token.should.be.a.sha1(); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js deleted file mode 100644 index 3fbca3f65..000000000 --- a/test/integration/utils/token-util_test.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var TokenUtil = require('../../../lib/utils/token-util'); -var should = require('should'); - -/** - * Test `TokenUtil` integration. - */ - -describe('TokenUtil integration', function() { - describe('generateRandomToken()', function() { - it('should return a sha-1 token', function() { - return TokenUtil.generateRandomToken() - .then(function(token) { - token.should.be.a.sha1; - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 00ecb38eb..000000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---require should ---require test/assertions ---ui bdd ---reporter spec diff --git a/test/tslint.json b/test/tslint.json new file mode 100755 index 000000000..8c62b6de9 --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,28 @@ +{ + "extends": ["tslint:recommended", "tslint:all", "tslint:latest"], + "jsRules": { + "no-unused-expression": true + }, + "rules": { + "file-name-casing": [true, "kebab-case"], + "no-unused-expression": false, + "semicolon": [true, "always", "ignore-bound-class-methods"], + "completed-docs": false, + "quotemark": [true, "single"], + "max-file-line-count": [false], + "member-access": [false], + "no-unsafe-any": false, + "promise-function-async": false, + "ordered-imports": [false], + "no-empty": [false], + "newline-per-chained-call": [false], + "max-line-length": [true, 120], + "no-magic-numbers": [false], + "member-ordering": [false], + "interface-name": [false], + "arrow-parens": false, + "no-implicit-dependencies": false, + "object-literal-sort-keys": false + }, + "rulesDirectory": [] +} diff --git a/test/unit/grant-types/abstract-grant-type.spec.ts b/test/unit/grant-types/abstract-grant-type.spec.ts new file mode 100755 index 000000000..12ab50977 --- /dev/null +++ b/test/unit/grant-types/abstract-grant-type.spec.ts @@ -0,0 +1,53 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { AbstractGrantType } from '../../../lib/grant-types'; + +/** + * Test `AbstractGrantType`. + */ + +describe('AbstractGrantType', () => { + describe('generateAccessToken()', () => { + it('should call `model.generateAccessToken()`', async () => { + const model = { + generateAccessToken: sinon + .stub() + .returns({ client: {}, expiresAt: new Date(), user: {} }), + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model, + }); + try { + await handler.generateAccessToken(); + model.generateAccessToken.callCount.should.equal(1); + model.generateAccessToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('generateRefreshToken()', () => { + it('should call `model.generateRefreshToken()`', async () => { + const model = { + generateRefreshToken: sinon.stub().returns({ + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model, + }); + try { + await handler.generateRefreshToken(); + model.generateRefreshToken.callCount.should.equal(1); + model.generateRefreshToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js deleted file mode 100644 index 528ca4041..000000000 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `AbstractGrantType`. - */ - -describe('AbstractGrantType', function() { - describe('generateAccessToken()', function() { - it('should call `model.generateAccessToken()`', function() { - var model = { - generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }) - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); - - return handler.generateAccessToken() - .then(function() { - model.generateAccessToken.callCount.should.equal(1); - model.generateAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('generateRefreshToken()', function() { - it('should call `model.generateRefreshToken()`', function() { - var model = { - generateRefreshToken: sinon.stub().returns({ client: {}, expiresAt: new Date(new Date() / 2), user: {} }) - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); - - return handler.generateRefreshToken() - .then(function() { - model.generateRefreshToken.callCount.should.equal(1); - model.generateRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/authorization-code-grant-type.spec.ts b/test/unit/grant-types/authorization-code-grant-type.spec.ts new file mode 100755 index 000000000..9ff43eaa0 --- /dev/null +++ b/test/unit/grant-types/authorization-code-grant-type.spec.ts @@ -0,0 +1,117 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { AuthorizationCodeGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `AuthorizationCodeGrantType`. + */ + +describe('AuthorizationCodeGrantType', () => { + describe('getAuthorizationCode()', () => { + it('should call `model.getAuthorizationCode()`', async () => { + const model = { + getAuthorizationCode: sinon.stub().returns({ + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }), + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client: any = {}; + try { + await handler.getAuthorizationCode(request, client); + + model.getAuthorizationCode.callCount.should.equal(1); + model.getAuthorizationCode.firstCall.args.should.have.length(1); + model.getAuthorizationCode.firstCall.args[0].should.equal(12345); + model.getAuthorizationCode.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('revokeAuthorizationCode()', () => { + it('should call `model.revokeAuthorizationCode()`', async () => { + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode: sinon.stub().returns(true), + saveToken() {}, + }; + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model, + }); + const authorizationCode: any = {}; + try { + await handler.revokeAuthorizationCode(authorizationCode); + + model.revokeAuthorizationCode.callCount.should.equal(1); + model.revokeAuthorizationCode.firstCall.args.should.have.length(1); + model.revokeAuthorizationCode.firstCall.args[0].should.equal( + authorizationCode, + ); + model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', async () => { + const client: any = {}; + const user = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken: sinon.stub().returns(true), + }; + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'validateScope').returns('foobiz' as any); + sinon + .stub(handler, 'generateAccessToken') + .returns(Promise.resolve('foo')); + sinon + .stub(handler, 'generateRefreshToken') + .returns(Promise.resolve('bar')); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + try { + await handler.saveToken(user, client, 'foobar', 'foobiz'); + 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[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js deleted file mode 100644 index 480416e68..000000000 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `AuthorizationCodeGrantType`. - */ - -describe('AuthorizationCodeGrantType', function() { - describe('getAuthorizationCode()', function() { - it('should call `model.getAuthorizationCode()`', function() { - var model = { - getAuthorizationCode: sinon.stub().returns({ authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), user: {} }), - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - var client = {}; - - return handler.getAuthorizationCode(request, client) - .then(function() { - model.getAuthorizationCode.callCount.should.equal(1); - model.getAuthorizationCode.firstCall.args.should.have.length(1); - model.getAuthorizationCode.firstCall.args[0].should.equal(12345); - model.getAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should call `model.revokeAuthorizationCode()`', function() { - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: sinon.stub().returns(true), - saveToken: function() {} - }; - var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - var authorizationCode = {}; - - return handler.revokeAuthorizationCode(authorizationCode) - .then(function() { - model.revokeAuthorizationCode.callCount.should.equal(1); - model.revokeAuthorizationCode.firstCall.args.should.have.length(1); - model.revokeAuthorizationCode.firstCall.args[0].should.equal(authorizationCode); - model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - - 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') - .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[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/client-credentials-grant-type.spec.ts b/test/unit/grant-types/client-credentials-grant-type.spec.ts new file mode 100755 index 000000000..693c7d669 --- /dev/null +++ b/test/unit/grant-types/client-credentials-grant-type.spec.ts @@ -0,0 +1,66 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ClientCredentialsGrantType } from '../../../lib/grant-types'; + +/** + * Test `ClientCredentialsGrantType`. + */ + +describe('ClientCredentialsGrantType', () => { + describe('getUserFromClient()', () => { + it('should call `model.getUserFromClient()`', async () => { + const model = { + getUserFromClient: sinon.stub().returns(true), + saveToken() {}, + }; + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const client: any = {}; + try { + await handler.getUserFromClient(client); + model.getUserFromClient.callCount.should.equal(1); + model.getUserFromClient.firstCall.args.should.have.length(1); + model.getUserFromClient.firstCall.args[0].should.equal(client); + model.getUserFromClient.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', async () => { + const client: any = {}; + const user = {}; + const model = { + getUserFromClient() {}, + saveToken: sinon.stub().returns(true), + }; + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'validateScope').returns('foobar' as any); + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + try { + await handler.saveToken(user, client, 'foobar'); + 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[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js deleted file mode 100644 index fe1fc4840..000000000 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `ClientCredentialsGrantType`. - */ - -describe('ClientCredentialsGrantType', function() { - describe('getUserFromClient()', function() { - it('should call `model.getUserFromClient()`', function() { - var model = { - getUserFromClient: sinon.stub().returns(true), - saveToken: function() {} - }; - var handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var client = {}; - - return handler.getUserFromClient(client) - .then(function() { - model.getUserFromClient.callCount.should.equal(1); - model.getUserFromClient.firstCall.args.should.have.length(1); - model.getUserFromClient.firstCall.args[0].should.equal(client); - model.getUserFromClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getUserFromClient: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'validateScope').returns('foobar'); - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - - 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[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/implict-grant-type.spec.ts b/test/unit/grant-types/implict-grant-type.spec.ts new file mode 100644 index 000000000..504f693e3 --- /dev/null +++ b/test/unit/grant-types/implict-grant-type.spec.ts @@ -0,0 +1,45 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ImplicitGrantType } from '../../../lib/grant-types'; +/** + * Test `ImplicitGrantType`. + */ + +describe('ImplicitGrantType', () => { + describe('saveToken()', () => { + it('should call `model.saveToken()`', () => { + const client = {}; + const user = {}; + const model = { + saveToken: sinon.stub().returns(true), + }; + const handler: any = new ImplicitGrantType({ + accessTokenLifetime: 120, + model, + user, + }); + + sinon.stub(handler, 'validateScope').returns('foobar-scope'); + sinon + .stub(handler, 'generateAccessToken') + .returns(Promise.resolve('foobar-token')); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('foo-1234'); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foobar-token', + accessTokenExpiresAt: 'foo-1234', + scope: 'foobar-scope', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/grant-types/password-grant-type.spec.ts b/test/unit/grant-types/password-grant-type.spec.ts new file mode 100755 index 000000000..c4f77beff --- /dev/null +++ b/test/unit/grant-types/password-grant-type.spec.ts @@ -0,0 +1,78 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { PasswordGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `PasswordGrantType`. + */ + +describe('PasswordGrantType', () => { + describe('getUser()', () => { + it('should call `model.getUser()`', async () => { + const model = { + getUser: sinon.stub().returns(true), + saveToken() {}, + }; + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await handler.getUser(request); + model.getUser.callCount.should.equal(1); + model.getUser.firstCall.args.should.have.length(2); + model.getUser.firstCall.args[0].should.equal('foo'); + model.getUser.firstCall.args[1].should.equal('bar'); + model.getUser.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', async () => { + const client: any = {}; + const user = {}; + const model = { + getUser() {}, + saveToken: sinon.stub().returns(true), + }; + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'validateScope').returns('foobar' as any); + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'generateRefreshToken').returns('bar' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + try { + await handler.saveToken(user, client, 'foobar'); + + 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[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js deleted file mode 100644 index 8e3bfc84e..000000000 --- a/test/unit/grant-types/password-grant-type_test.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `PasswordGrantType`. - */ - -describe('PasswordGrantType', function() { - describe('getUser()', function() { - it('should call `model.getUser()`', function() { - var model = { - getUser: sinon.stub().returns(true), - saveToken: function() {} - }; - var handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.getUser(request) - .then(function() { - model.getUser.callCount.should.equal(1); - model.getUser.firstCall.args.should.have.length(2); - model.getUser.firstCall.args[0].should.equal('foo'); - model.getUser.firstCall.args[1].should.equal('bar'); - model.getUser.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getUser: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); - - 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') - .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[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/refresh-token-grant-type.spec.ts b/test/unit/grant-types/refresh-token-grant-type.spec.ts new file mode 100755 index 000000000..6be1ed92c --- /dev/null +++ b/test/unit/grant-types/refresh-token-grant-type.spec.ts @@ -0,0 +1,298 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { RefreshTokenGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `RefreshTokenGrantType`. + */ + +describe('RefreshTokenGrantType', () => { + describe('handle()', () => { + it('should revoke the previous token', () => { + const token = { accessToken: 'foo', client: {}, user: {} }; + const model = { + getRefreshToken() { + return token; + }, + saveToken() { + return { accessToken: 'bar', client: {}, user: {} }; + }, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client: any = {}; + + return handler + .handle(request, client) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('getRefreshToken()', () => { + it('should call `model.getRefreshToken()`', () => { + const model = { + getRefreshToken: sinon + .stub() + .returns({ accessToken: 'foo', client: {}, user: {} }), + saveToken() {}, + revokeToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client: any = {}; + + return handler + .getRefreshToken(request, client) + .then(() => { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal('bar'); + model.getRefreshToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('revokeToken()', () => { + it('should call `model.revokeToken()`', () => { + const model = { + getRefreshToken() {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + saveToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const token: any = {}; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should not call `model.revokeToken()`', () => { + const model = { + getRefreshToken() {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + saveToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: false, + }); + const token: any = {}; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(0); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should not call `model.revokeToken()`', () => { + const model = { + getRefreshToken() {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + saveToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: true, + }); + const token: any = {}; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', () => { + const client: any = {}; + const user = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken: sinon.stub().returns(true), + }; + const handler: any = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + 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') + .then(() => { + 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[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should call `model.saveToken()` without refresh token', () => { + const client = {}; + const user = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken: sinon.stub().returns(true), + }; + const handler: any = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: false, + }); + + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'generateRefreshToken').returns('bar' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + 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[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should call `model.saveToken()` with refresh token', () => { + const client = {}; + const user = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken: sinon.stub().returns(true), + }; + const handler: any = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: true, + }); + + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'generateRefreshToken').returns('bar' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + 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[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js deleted file mode 100644 index e5693ba8f..000000000 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ /dev/null @@ -1,200 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `RefreshTokenGrantType`. - */ - -describe('RefreshTokenGrantType', function() { - describe('handle()', function() { - it('should revoke the previous token', function() { - var token = { accessToken: 'foo', client: {}, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - saveToken: function() { return { accessToken: 'bar', client: {}, user: {} }; }, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); - var client = {}; - - return handler.handle(request, client) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getRefreshToken()', function() { - it('should call `model.getRefreshToken()`', function() { - var model = { - getRefreshToken: sinon.stub().returns({ accessToken: 'foo', client: {}, user: {} }), - saveToken: function() {}, - revokeToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); - var client = {}; - - return handler.getRefreshToken(request, client) - .then(function() { - model.getRefreshToken.callCount.should.equal(1); - model.getRefreshToken.firstCall.args.should.have.length(1); - model.getRefreshToken.firstCall.args[0].should.equal('bar'); - model.getRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('revokeToken()', function() { - it('should call `model.revokeToken()`', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should not call `model.revokeToken()`', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); - var token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(0); - }) - .catch(should.fail); - }); - - it('should not call `model.revokeToken()`', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true }); - var token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - 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') - .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[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveToken()` without refresh token', function() { - var client = {}; - var user = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); - - 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') - .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[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveToken()` with refresh token', function() { - var client = {}; - var user = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true}); - - 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') - .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[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/handlers/authenticate-handler.spec.ts b/test/unit/handlers/authenticate-handler.spec.ts new file mode 100755 index 000000000..442ef0f3c --- /dev/null +++ b/test/unit/handlers/authenticate-handler.spec.ts @@ -0,0 +1,166 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ServerError } from '../../../lib/errors'; +import { AuthenticateHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; + +/** + * Test `AuthenticateHandler`. + */ + +describe('AuthenticateHandler', () => { + describe('getTokenFromRequest()', () => { + describe('with bearer token in the request authorization header', () => { + it('should call `getTokenFromRequestHeader()`', () => { + const handler: any = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + + sinon.stub(handler, 'getTokenFromRequestHeader'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestHeader.callCount.should.equal(1); + handler.getTokenFromRequestHeader.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestHeader.restore(); + }); + }); + + describe('with bearer token in the request query', () => { + it('should call `getTokenFromRequestQuery()`', () => { + const handler: any = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { access_token: 'foo' }, + }); + + sinon.stub(handler, 'getTokenFromRequestQuery'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestQuery.callCount.should.equal(1); + handler.getTokenFromRequestQuery.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestQuery.restore(); + }); + }); + + describe('with bearer token in the request body', () => { + it('should call `getTokenFromRequestBody()`', () => { + const handler: any = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + sinon.stub(handler, 'getTokenFromRequestBody'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestBody.callCount.should.equal(1); + handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); + handler.getTokenFromRequestBody.restore(); + }); + }); + }); + + describe('getAccessToken()', () => { + it('should call `model.getAccessToken()`', () => { + const model = { + getAccessToken: sinon.stub().returns({ user: {} }), + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(() => { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args.should.have.length(1); + model.getAccessToken.firstCall.args[0].should.equal('foo'); + model.getAccessToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('validateAccessToken()', () => { + it('should fail if token has no valid `accessTokenExpiresAt` date', () => { + const model = { + getAccessToken() {}, + }; + const handler = new AuthenticateHandler({ model }); + + let failed = false; + try { + handler.validateAccessToken({ + user: {}, + } as any); + } catch (err) { + err.should.be.an.instanceOf(ServerError); + failed = true; + } + failed.should.equal(true); + }); + + it('should succeed if token has valid `accessTokenExpiresAt` date', () => { + const model = { + getAccessToken() {}, + }; + const handler = new AuthenticateHandler({ model }); + try { + handler.validateAccessToken({ + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + } as any); + } catch (err) { + should.fail('should.fail', ''); + } + }); + }); + + describe('verifyScope()', () => { + it('should call `model.getAccessToken()` if scope is defined', () => { + const model = { + getAccessToken() {}, + verifyScope: sinon.stub().returns(true), + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'bar', + }); + + return handler + .verifyScope('foo' as any) + .then(() => { + 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.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js deleted file mode 100644 index 2adac7884..000000000 --- a/test/unit/handlers/authenticate-handler_test.js +++ /dev/null @@ -1,153 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); -var ServerError = require('../../../lib/errors/server-error'); - -/** - * Test `AuthenticateHandler`. - */ - -describe('AuthenticateHandler', function() { - describe('getTokenFromRequest()', function() { - describe('with bearer token in the request authorization header', function() { - it('should call `getTokenFromRequestHeader()`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - - sinon.stub(handler, 'getTokenFromRequestHeader'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestHeader.callCount.should.equal(1); - handler.getTokenFromRequestHeader.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestHeader.restore(); - }); - }); - - describe('with bearer token in the request query', function() { - it('should call `getTokenFromRequestQuery()`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { access_token: 'foo' } - }); - - sinon.stub(handler, 'getTokenFromRequestQuery'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestQuery.callCount.should.equal(1); - handler.getTokenFromRequestQuery.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestQuery.restore(); - }); - }); - - describe('with bearer token in the request body', function() { - it('should call `getTokenFromRequestBody()`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: {}, - query: {} - }); - - sinon.stub(handler, 'getTokenFromRequestBody'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestBody.callCount.should.equal(1); - handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestBody.restore(); - }); - }); - }); - - describe('getAccessToken()', function() { - it('should call `model.getAccessToken()`', function() { - var model = { - getAccessToken: sinon.stub().returns({ user: {} }) - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(function() { - model.getAccessToken.callCount.should.equal(1); - model.getAccessToken.firstCall.args.should.have.length(1); - model.getAccessToken.firstCall.args[0].should.equal('foo'); - model.getAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('validateAccessToken()', function() { - it('should fail if token has no valid `accessTokenExpiresAt` date', function() { - var model = { - getAccessToken: function() {} - }; - var handler = new AuthenticateHandler({ model: model }); - - var failed = false; - try { - handler.validateAccessToken({ - user: {} - }); - } - catch (err) { - err.should.be.an.instanceOf(ServerError); - failed = true; - } - failed.should.equal(true); - }); - - it('should succeed if token has valid `accessTokenExpiresAt` date', function() { - var model = { - getAccessToken: function() {} - }; - var handler = new AuthenticateHandler({ model: model }); - try { - handler.validateAccessToken({ - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }); - } - catch (err) { - should.fail(); - } - }); - }); - - describe('verifyScope()', function() { - it('should call `model.getAccessToken()` if scope is defined', function() { - var model = { - getAccessToken: function() {}, - verifyScope: sinon.stub().returns(true) - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'bar' }); - - 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.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/handlers/authorize-handler.spec.ts b/test/unit/handlers/authorize-handler.spec.ts new file mode 100755 index 000000000..04c010f08 --- /dev/null +++ b/test/unit/handlers/authorize-handler.spec.ts @@ -0,0 +1,140 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { AuthorizeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; + +/** + * Test `AuthorizeHandler`. + */ + +describe('AuthorizeHandler', () => { + // describe('generateAuthorizationCode()', () => { + // it('should call `model.generateAuthorizationCode()`', async () => { + // const model = { + // generateAuthorizationCode: sinon.stub().returns({}), + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // try { + // await handler.generateAuthorizationCode({}, {}, {}); + // model.generateAuthorizationCode.callCount.should.equal(1); + // model.generateAuthorizationCode.firstCall.thisValue.should.equal(model); + // } catch (error) { + // should.fail('should.fail', ''); + // } + // }); + // }); + + describe('getClient()', () => { + it('should call `model.getClient()`', async () => { + const model = { + getAccessToken() {}, + getClient: sinon.stub().returns( + Promise.resolve({ + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }), + ), + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await handler.getClient(request); + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(1); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('getUser()', () => { + it('should call `authenticateHandler.getUser()`', () => { + const authenticateHandler = { + handle: sinon.stub().returns(Promise.resolve({})), + }; + const model = { + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(() => { + authenticateHandler.handle.callCount.should.equal(1); + authenticateHandler.handle.firstCall.args.should.have.length(2); + authenticateHandler.handle.firstCall.args[0].should.equal(request); + authenticateHandler.handle.firstCall.args[1].should.equal(response); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + // describe('saveAuthorizationCode()', () => { + // it('should call `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode: sinon.stub().returns({}), + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // return handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'qux', + // 'biz' as any, + // 'baz', + // 'boz' as any, + // ) + // .then(() => { + // 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[1].should.equal('biz'); + // model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + // model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + // }) + // .catch(() => should.fail('should.fail', '')); + // }); + // }); +}); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js deleted file mode 100644 index fe9b6b1d7..000000000 --- a/test/unit/handlers/authorize-handler_test.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `AuthorizeHandler`. - */ - -describe('AuthorizeHandler', function() { - describe('generateAuthorizationCode()', function() { - it('should call `model.generateAuthorizationCode()`', function() { - var model = { - generateAuthorizationCode: sinon.stub().returns({}), - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.generateAuthorizationCode() - .then(function() { - model.generateAuthorizationCode.callCount.should.equal(1); - model.generateAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getClient()', function() { - it('should call `model.getClient()`', function() { - var model = { - getAccessToken: function() {}, - getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }), - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getUser()', function() { - it('should call `authenticateHandler.getUser()`', function() { - var authenticateHandler = { handle: sinon.stub().returns(Promise.resolve({})) }; - var model = { - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - var response = new Response(); - - return handler.getUser(request, response) - .then(function() { - authenticateHandler.handle.callCount.should.equal(1); - authenticateHandler.handle.firstCall.args.should.have.length(2); - authenticateHandler.handle.firstCall.args[0].should.equal(request); - authenticateHandler.handle.firstCall.args[1].should.equal(response); - }) - .catch(should.fail); - }); - }); - - describe('saveAuthorizationCode()', function() { - it('should call `model.saveAuthorizationCode()`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: sinon.stub().returns({}) - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - 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[1].should.equal('biz'); - model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); - model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/handlers/revoke-handlers.spec.ts b/test/unit/handlers/revoke-handlers.spec.ts new file mode 100644 index 000000000..70fc355ea --- /dev/null +++ b/test/unit/handlers/revoke-handlers.spec.ts @@ -0,0 +1,126 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { RevokeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; + +/** + * Test `RevokeHandler`. + */ + +describe('RevokeHandler', () => { + describe('handleRevokeToken()', () => { + it('should call `model.getAccessToken()` and `model.getRefreshToken()`', () => { + const model = { + getClient() {}, + revokeToken: sinon.stub().returns(true), + getRefreshToken: sinon.stub().returns({ + refreshToken: 'hash', + client: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }), + getAccessToken: sinon.stub().returns(false), + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client = {}; + + return handler + .handleRevokeToken(request, client) + .then(() => { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args[0].should.equal('foo'); + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args[0].should.equal('foo'); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', () => { + it('should call `model.getClient()`', () => { + const model = { + getClient: sinon.stub().returns({ grants: ['password'] }), + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal('secret'); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', () => { + it('should call `model.getRefreshToken()`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() {}, + getRefreshToken: sinon.stub().returns({ + refreshToken: 'hash', + client: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }), + }; + const handler: any = new RevokeHandler({ model }); + const token = 'hash'; + const client = {}; + + return handler + .getRefreshToken(token, client) + .then(() => { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('revokeToken()', () => { + it('should call `model.revokeToken()`', () => { + const model = { + getClient() {}, + revokeToken: sinon.stub().returns(true), + getRefreshToken: sinon.stub().returns({ + refreshToken: 'hash', + client: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }), + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const token = 'hash'; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/handlers/token-handler.spec.ts b/test/unit/handlers/token-handler.spec.ts new file mode 100755 index 000000000..53fcd36cf --- /dev/null +++ b/test/unit/handlers/token-handler.spec.ts @@ -0,0 +1,45 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { TokenHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; + +/** + * Test `TokenHandler`. + */ + +describe('TokenHandler', () => { + describe('getClient()', () => { + it('should call `model.getClient()`', () => { + const model = { + getClient: sinon + .stub() + .returns(Promise.resolve({ grants: ['password'] })), + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, {}) + .then(() => { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal('secret'); + model.getClient.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js deleted file mode 100644 index 2b37cd05a..000000000 --- a/test/unit/handlers/token-handler_test.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Request = require('../../../lib/request'); -var TokenHandler = require('../../../lib/handlers/token-handler'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `TokenHandler`. - */ - -describe('TokenHandler', function() { - describe('getClient()', function() { - it('should call `model.getClient()`', function() { - var model = { - getClient: sinon.stub().returns({ grants: ['password'] }), - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.args[1].should.equal('secret'); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/models/token-model.spec.ts b/test/unit/models/token-model.spec.ts new file mode 100755 index 000000000..b59f488c9 --- /dev/null +++ b/test/unit/models/token-model.spec.ts @@ -0,0 +1,25 @@ +import { TokenModel } from '../../../lib/models'; + +/** + * Test `Server`. + */ + +describe('Model', () => { + describe('constructor()', () => { + it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', () => { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + accessTokenExpiresAt: atExpiresAt, + }; + + const model = new TokenModel(data); + model.accessTokenLifetime.should.be.Number(); + model.accessTokenLifetime.should.be.approximately(3600, 2); + }); + }); +}); diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js deleted file mode 100644 index 3d899951c..000000000 --- a/test/unit/models/token-model_test.js +++ /dev/null @@ -1,25 +0,0 @@ -var TokenModel = require('../../../lib/models/token-model'); - -/** - * Test `Server`. - */ - -describe('Model', function() { - describe('constructor()', function() { - it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', function() { - var atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - var data = { - accessToken: 'foo', - client: 'bar', - user: 'tar', - accessTokenExpiresAt: atExpiresAt - }; - - var model = new TokenModel(data); - model.accessTokenLifetime.should.be.Number; - model.accessTokenLifetime.should.be.approximately(3600, 2); - }); - }); -}); diff --git a/test/unit/request.spec.ts b/test/unit/request.spec.ts new file mode 100755 index 000000000..16635b1c3 --- /dev/null +++ b/test/unit/request.spec.ts @@ -0,0 +1,170 @@ +import * as should from 'should'; +import { Request } from '../../lib/request'; + +/** + * Test `Request`. + */ + +function generateBaseRequest() { + return { + query: { + foo: 'bar', + } as any, + method: 'GET', + headers: { + bar: 'foo', + } as any, + body: { + foobar: 'barfoo', + } as any, + } as any; +} + +describe('Request', () => { + it('should instantiate with a basic request', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + }); + + it('should allow a request to be passed without a body', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.body; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql({}); + }); + + it('should throw if headers are not passed to the constructor', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.headers; + + (() => { + new Request(originalRequest); + }).should.throw('Missing parameter: `headers`'); + }); + + it('should throw if query string isnt passed to the constructor', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.query; + + (() => { + new Request(originalRequest); + }).should.throw('Missing parameter: `query`'); + }); + + it('should throw if method isnt passed to the constructor', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.method; + + (() => { + new Request(originalRequest); + }).should.throw('Missing parameter: `method`'); + }); + + it('should convert all header keys to lowercase', () => { + const originalRequest = generateBaseRequest(); + originalRequest.headers = { + Foo: 'bar', + BAR: 'foo', + } as any; + + const request = new Request(originalRequest); + request.headers.foo.should.eql('bar'); + request.headers.bar.should.eql('foo'); + should.not.exist(request.headers.Foo); + should.not.exist(request.headers.BAR); + }); + + it('should include additional properties passed in the request', () => { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: 'newBar', + }; + + originalRequest.custom2 = { + newBar: 'newFoo', + }; + + const request: any = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it('should include additional properties passed in the request', () => { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: 'newBar', + }; + + originalRequest.custom2 = { + newBar: 'newFoo', + }; + + const request: any = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it('should allow getting of headers using `request.get`', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should allow getting of headers using `request.get`', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should allow getting of headers using `request.get`', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should validate the content-type', () => { + const originalRequest = generateBaseRequest(); + originalRequest.headers['content-type'] = + 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request + .is('application/x-www-form-urlencoded') + .should.eql('application/x-www-form-urlencoded'); + }); + + it('should return false if the content-type is invalid', () => { + const originalRequest = generateBaseRequest(); + originalRequest.headers['content-type'] = + 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request.is('application/json').should.be.false(); + }); +}); diff --git a/test/unit/request_test.js b/test/unit/request_test.js deleted file mode 100644 index 458cb8f9e..000000000 --- a/test/unit/request_test.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Request = require('../../lib/request'); -var should = require('should'); - -/** - * Test `Request`. - */ - -function generateBaseRequest() { - return { - query: { - foo: 'bar' - }, - method: 'GET', - headers: { - bar: 'foo' - }, - body: { - foobar: 'barfoo' - } - }; -} - -describe('Request', function() { - it('should instantiate with a basic request', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - }); - - it('should allow a request to be passed without a body', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.body; - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql({}); - }); - - it('should throw if headers are not passed to the constructor', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.headers; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `headers`'); - }); - - it('should throw if query string isn\'t passed to the constructor', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.query; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `query`'); - }); - - it('should throw if method isn\'t passed to the constructor', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.method; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `method`'); - }); - - it('should convert all header keys to lowercase', function() { - var originalRequest = generateBaseRequest(); - originalRequest.headers = { - Foo: 'bar', - BAR: 'foo' - }; - - var request = new Request(originalRequest); - request.headers.foo.should.eql('bar'); - request.headers.bar.should.eql('foo'); - should.not.exist(request.headers.Foo); - should.not.exist(request.headers.BAR); - }); - - it('should include additional properties passed in the request', function() { - var originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: 'newBar' - }; - - originalRequest.custom2 = { - newBar: 'newFoo' - }; - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it('should include additional properties passed in the request', function() { - var originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: 'newBar' - }; - - originalRequest.custom2 = { - newBar: 'newFoo' - }; - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it('should allow getting of headers using `request.get`', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should allow getting of headers using `request.get`', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should allow getting of headers using `request.get`', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should validate the content-type', function() { - var originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; - - var request = new Request(originalRequest); - request.is('application/x-www-form-urlencoded').should.eql('application/x-www-form-urlencoded'); - }); - - it('should return false if the content-type is invalid', function() { - var originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; - - var request = new Request(originalRequest); - request.is('application/json').should.eql(false); - }); -}); diff --git a/test/unit/response.spec.ts b/test/unit/response.spec.ts new file mode 100755 index 000000000..2a2c3f9a9 --- /dev/null +++ b/test/unit/response.spec.ts @@ -0,0 +1,114 @@ +import * as should from 'should'; +import { Response } from '../../lib/response'; + +/** + * Test `Request`. + */ + +const generateBaseResponse = () => { + return { + headers: { + bar: 'foo', + } as any, + body: { + foobar: 'barfoo', + } as any, + } as any; +}; + +describe('Response', () => { + it('should instantiate with a basic request', () => { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it('should allow a response to be passed without a body', () => { + const originalResponse = generateBaseResponse(); + delete originalResponse.body; + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql({}); + response.status.should.eql(200); + }); + + it('should allow a response to be passed without headers', () => { + const originalResponse = generateBaseResponse(); + delete originalResponse.headers; + + const response = new Response(originalResponse); + response.headers.should.eql({}); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it('should convert all header keys to lowercase', () => { + const originalResponse = generateBaseResponse(); + originalResponse.headers = { + Foo: 'bar', + BAR: 'foo', + }; + + const response: any = new Response(originalResponse); + response.headers.foo.should.eql('bar'); + response.headers.bar.should.eql('foo'); + should.not.exist(response.headers.Foo); + should.not.exist(response.headers.BAR); + }); + + it('should include additional properties passed in the response', () => { + const originalResponse = generateBaseResponse(); + originalResponse.custom = { + newFoo: 'newBar', + }; + + originalResponse.custom2 = { + newBar: 'newFoo', + }; + + const response: any = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.custom.should.eql(originalResponse.custom); + response.custom2.should.eql(originalResponse.custom2); + }); + + it('should allow getting of headers using `response.get`', () => { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get('bar').should.eql(originalResponse.headers.bar); + }); + + it('should allow getting of headers using `response.get`', () => { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get('bar').should.eql(originalResponse.headers.bar); + }); + + it('should allow setting of headers using `response.set`', () => { + const originalResponse = generateBaseResponse(); + + const response: any = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.set('new_header', 'new_value'); + response.headers.bar.should.eql('foo'); + response.headers.new_header.should.eql('new_value'); + }); + + it('should process redirect', () => { + const originalResponse = generateBaseResponse(); + + const response: any = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.status.should.eql(200); + response.redirect('http://foo.bar'); + response.headers.location.should.eql('http://foo.bar'); + response.status.should.eql(302); + }); +}); diff --git a/test/unit/response_test.js b/test/unit/response_test.js deleted file mode 100644 index c435e32f7..000000000 --- a/test/unit/response_test.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Response = require('../../lib/response'); -var should = require('should'); - -/** - * Test `Request`. - */ - -function generateBaseResponse() { - return { - headers: { - bar: 'foo' - }, - body: { - foobar: 'barfoo' - } - }; -} - -describe('Request', function() { - it('should instantiate with a basic request', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it('should allow a response to be passed without a body', function() { - var originalResponse = generateBaseResponse(); - delete originalResponse.body; - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql({}); - response.status.should.eql(200); - }); - - it('should allow a response to be passed without headers', function() { - var originalResponse = generateBaseResponse(); - delete originalResponse.headers; - - var response = new Response(originalResponse); - response.headers.should.eql({}); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it('should convert all header keys to lowercase', function() { - var originalResponse = generateBaseResponse(); - originalResponse.headers = { - Foo: 'bar', - BAR: 'foo' - }; - - var response = new Response(originalResponse); - response.headers.foo.should.eql('bar'); - response.headers.bar.should.eql('foo'); - should.not.exist(response.headers.Foo); - should.not.exist(response.headers.BAR); - }); - - it('should include additional properties passed in the response', function() { - var originalResponse = generateBaseResponse(); - originalResponse.custom = { - newFoo: 'newBar' - }; - - originalResponse.custom2 = { - newBar: 'newFoo' - }; - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.custom.should.eql(originalResponse.custom); - response.custom2.should.eql(originalResponse.custom2); - }); - - it('should allow getting of headers using `response.get`', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.get('bar').should.eql(originalResponse.headers.bar); - }); - - it('should allow getting of headers using `response.get`', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.get('bar').should.eql(originalResponse.headers.bar); - }); - - it('should allow setting of headers using `response.set`', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.set('newheader', 'newvalue'); - response.headers.bar.should.eql('foo'); - response.headers.newheader.should.eql('newvalue'); - }); - - it('should process redirect', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.status.should.eql(200); - response.redirect('http://foo.bar'); - response.headers.location.should.eql('http://foo.bar'); - response.status.should.eql(302); - }); -}); diff --git a/test/unit/server.spec.ts b/test/unit/server.spec.ts new file mode 100755 index 000000000..2ce3b4f7d --- /dev/null +++ b/test/unit/server.spec.ts @@ -0,0 +1,91 @@ +import * as sinon from 'sinon'; +import { + AuthenticateHandler, + AuthorizeHandler, + TokenHandler, +} from '../../lib/handlers'; +import { OAuth2Server as Server } from '../../lib/server'; + +const Authenticate: any = AuthenticateHandler; +const Authorize: any = AuthorizeHandler; +const Token: any = TokenHandler; +/** + * Test `Server`. + */ + +describe('Server', () => { + describe('authenticate()', () => { + it('should call `handle`', async () => { + const model = { + getAccessToken() {}, + }; + const server = new Server({ model }); + + sinon.stub(Authenticate.prototype, 'handle').returns(Promise.resolve()); + + await server.authenticate('foo' as any); + + Authenticate.prototype.handle.callCount.should.equal(1); + Authenticate.prototype.handle.firstCall.args[0].should.equal('foo'); + Authenticate.prototype.handle.restore(); + }); + + it('should map string passed as `options` to `options.scope`', async () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const server = new Server({ model }); + + sinon.stub(Authenticate.prototype, 'handle').returns(Promise.resolve()); + + await server.authenticate('foo' as any, 'bar' as any, 'test'); + + Authenticate.prototype.handle.callCount.should.equal(1); + Authenticate.prototype.handle.firstCall.args[0].should.equal('foo'); + Authenticate.prototype.handle.firstCall.args[1].should.equal('bar'); + Authenticate.prototype.handle.firstCall.thisValue.should.have.property( + 'scope', + 'test', + ); + Authenticate.prototype.handle.restore(); + }); + }); + + describe('authorize()', () => { + it('should call `handle`', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const server = new Server({ model }); + + sinon.stub(Authorize.prototype, 'handle').returns(Promise.resolve()); + + await server.authorize('foo' as any, 'bar' as any); + + Authorize.prototype.handle.callCount.should.equal(1); + Authorize.prototype.handle.firstCall.args[0].should.equal('foo'); + Authorize.prototype.handle.restore(); + }); + }); + + describe('token()', () => { + it('should call `handle`', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const server = new Server({ model }); + + sinon.stub(Token.prototype, 'handle').returns(Promise.resolve()); + + await server.token('foo' as any, 'bar' as any); + + Token.prototype.handle.callCount.should.equal(1); + Token.prototype.handle.firstCall.args[0].should.equal('foo'); + Token.prototype.handle.restore(); + }); + }); +}); diff --git a/test/unit/server_test.js b/test/unit/server_test.js deleted file mode 100644 index e7c343f0c..000000000 --- a/test/unit/server_test.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthenticateHandler = require('../../lib/handlers/authenticate-handler'); -var AuthorizeHandler = require('../../lib/handlers/authorize-handler'); -var Promise = require('bluebird'); -var Server = require('../../lib/server'); -var TokenHandler = require('../../lib/handlers/token-handler'); -var sinon = require('sinon'); - -/** - * Test `Server`. - */ - -describe('Server', function() { - describe('authenticate()', function() { - it('should call `handle`', function() { - var model = { - getAccessToken: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); - - server.authenticate('foo'); - - AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthenticateHandler.prototype.handle.restore(); - }); - - it('should map string passed as `options` to `options.scope`', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var 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() { - it('should call `handle`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(AuthorizeHandler.prototype, 'handle').returns(Promise.resolve()); - - server.authorize('foo', 'bar'); - - AuthorizeHandler.prototype.handle.callCount.should.equal(1); - AuthorizeHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthorizeHandler.prototype.handle.restore(); - }); - }); - - describe('token()', function() { - it('should call `handle`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(TokenHandler.prototype, 'handle').returns(Promise.resolve()); - - server.token('foo', 'bar'); - - TokenHandler.prototype.handle.callCount.should.equal(1); - TokenHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - TokenHandler.prototype.handle.restore(); - }); - }); -}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100755 index 000000000..56ee33060 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": true + }, + "exclude": ["node_modules", "test", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 000000000..b42af265a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "target": "es2017", + "sourceMap": true, + "importHelpers": true, + "outDir": "./dist", + "baseUrl": ".", + "typeRoots": ["node_modules/@types"], + "downlevelIteration": false, + "emitDecoratorMetadata": false, + "experimentalDecorators": false, + "moduleResolution": "node", + "lib": ["es2018"] + }, + "exclude": ["node_modules", "./dist"] +} diff --git a/tslint.json b/tslint.json new file mode 100755 index 000000000..db341dfc4 --- /dev/null +++ b/tslint.json @@ -0,0 +1,28 @@ +{ + "extends": ["tslint:recommended", "tslint:all", "tslint:latest"], + "jsRules": { + "no-unused-expression": true + }, + "rules": { + "arrow-parens": false, + "completed-docs": false, + "file-name-casing": [true, "kebab-case"], + "no-object-literal-type-assertion": [false], + "interface-name": [false], + "max-line-length": [true, 120], + "member-access": [false], + "no-boolean-literal-compare": [false], + "member-ordering": [false], + "no-default-export": [false], + "prefer-function-over-method": [false], + "no-magic-numbers": [false], + "newline-per-chained-call": false, + "no-unsafe-any": false, + "object-literal-sort-keys": false, + "ordered-imports": [false], + "quotemark": [true, "single"], + "semicolon": [true, "always", "ignore-bound-class-methods"], + "strict-boolean-expressions": [false] + }, + "rulesDirectory": [] +}