diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..a57aeddc4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,36 @@ +{ + "env": { + "jquery": true, + "mocha": true, + "node": true + }, + "globals": {}, + "rules": { + "no-bitwise": 2, + "curly": 2, + "eqeqeq": 2, + "no-unused-expressions": 2, + "strict": 0, + "wrap-iife": [ + 2, + "any" + ], + "indent": [ + 2, + 2, + { + "SwitchCase": 1 + } + ], + "no-use-before-define": 0, + "new-cap": 2, + "no-caller": 2, + "require-yield": 2, + "quotes": [ + 2, + "single" + ], + "no-undef": 2, + "no-unused-vars": 2 + } +} diff --git a/.gitignore b/.gitignore index 820d105cc..08b44961f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,35 @@ node_modules/ docs/_build/ __pycache__/ *.pyc +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz +*.iml +.idea +.jshint +.DS_Store + +pids +logs +results + +lib/dockerImage/keys +coverage +npm-debug.log*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive 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/.travis.yml b/.travis.yml index 1ba9eefaa..045099f9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,10 @@ language: node_js node_js: - - 4 - - 4.0 - 6 - - 6.0 - 7 - - 7.0 - 8 - - 8.0 + - 9 + - 10 sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d784f6e6..c616fd136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ ## Changelog +### 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 eslint + +### 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/README.md b/README.md index 9cf373748..33a85901d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ 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/rfc6750.html) compliant. - Implicitly supports any form of storage, e.g. *PostgreSQL*, *MySQL*, *MongoDB*, *Redis*, etc. @@ -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/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 199de9383..9d03c8f28 100644 --- a/docs/misc/migrating-v2-to-v3.rst +++ b/docs/misc/migrating-v2-to-v3.rst @@ -28,17 +28,17 @@ 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 changed behavior in v3.0.0: @@ -60,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 @@ -75,7 +75,7 @@ Model specification * `user` (`Object`) * `getClient(clientId, clientSecret)` should return an object with, at minimum: - + * `redirectUris` (`Array`) * `grants` (`Array`) @@ -88,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/lib/errors/server-error.js b/lib/errors/server-error.js index d193af39c..0a2bcf1f0 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -18,7 +18,7 @@ var util = require('util'); function ServerError(message, properties) { properties = _.assign({ - code: 503, + code: 500, name: 'server_error' }, properties); diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index be4259dec..224a473e3 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -67,11 +67,7 @@ AbstractGrantType.prototype.generateRefreshToken = function(client, user, scope) */ AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); - - return expires; + return new Date(Date.now() + this.accessTokenLifetime * 1000); }; /** @@ -79,11 +75,7 @@ AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { */ AbstractGrantType.prototype.getRefreshTokenExpiresAt = function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); - - return expires; + return new Date(Date.now() + this.refreshTokenLifetime * 1000); }; /** diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 7eae70f8f..97c126793 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -133,21 +133,21 @@ AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, cl * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ - AuthorizationCodeGrantType.prototype.validateRedirectUri = function(request, code) { - if (!code.redirectUri) { - return; - } +AuthorizationCodeGrantType.prototype.validateRedirectUri = function(request, code) { + if (!code.redirectUri) { + return; + } - var redirectUri = request.body.redirect_uri || request.query.redirect_uri; + 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 (!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'); - } - }; + if (redirectUri !== code.redirectUri) { + throw new InvalidRequestError('Invalid request: `redirect_uri` is invalid'); + } +}; /** * Revoke the authorization code. diff --git a/lib/grant-types/implicit-grant-type.js b/lib/grant-types/implicit-grant-type.js new file mode 100644 index 000000000..f79963b3b --- /dev/null +++ b/lib/grant-types/implicit-grant-type.js @@ -0,0 +1,88 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var AbstractGrantType = require('./abstract-grant-type'); +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var Promise = require('bluebird'); +var promisify = require('promisify-any').use(Promise); +var util = require('util'); + +/** + * Constructor. + */ + +function ImplicitGrantType(options) { + options = 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; + + AbstractGrantType.call(this, options); +} + +/** + * Inherit prototype. + */ + +util.inherits(ImplicitGrantType, AbstractGrantType); + +/** + * Handle implicit token grant. + */ + +ImplicitGrantType.prototype.handle = function(request, 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. + */ + +ImplicitGrantType.prototype.saveToken = function(user, client, scope) { + var fns = [ + this.validateScope(user, client, scope), + this.generateAccessToken(client, user, scope), + this.getAccessTokenExpiresAt() + ]; + + 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 = ImplicitGrantType; diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index dc9117b27..590c476a2 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -63,6 +63,9 @@ AuthenticateHandler.prototype.handle = function(request, response) { throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); } + // Extend model object with request + this.model.request = request; + return Promise.bind(this) .then(function() { return this.getTokenFromRequest(request); diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 984136a8d..3c14d2290 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -20,7 +20,6 @@ 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'); /** @@ -29,7 +28,7 @@ var url = require('url'); var responseTypes = { code: require('../response-types/code-response-type'), - //token: require('../response-types/token-response-type') + token: require('../response-types/token-response-type') }; /** @@ -43,10 +42,6 @@ function AuthorizeHandler(options) { 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`'); } @@ -55,13 +50,9 @@ function AuthorizeHandler(options) { 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.options = options; this.allowEmptyState = options.allowEmptyState; this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); - this.authorizationCodeLifetime = options.authorizationCodeLifetime; this.model = options.model; } @@ -82,75 +73,60 @@ AuthorizeHandler.prototype.handle = function(request, response) { return Promise.reject(new AccessDeniedError('Access denied: user denied access to application')); } + // Extend model object with request + this.model.request = request; + 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); + .spread(function(client, user) { var scope; var state; - var ResponseType; + var RequestedResponseType; + var responseType; + var uri = this.getRedirectUri(request, client); - return Promise.bind(this) - .then(function() { - scope = this.getScope(request); + return Promise + .bind(this) + .then(function() { + var requestedScope = this.getScope(request); - return this.generateAuthorizationCode(client, user, scope); - }) - .then(function(authorizationCode) { + return this.validateScope(user, client, requestedScope); + }) + .then(function(validScope) { + scope = validScope; state = this.getState(request); - ResponseType = this.getResponseType(request); - return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user); + RequestedResponseType = this.getResponseType(request, client); + responseType = new RequestedResponseType(this.options); + + return responseType.handle(request, client, user, uri, scope); }) - .then(function(code) { - var responseType = new ResponseType(code.authorizationCode); + .then(function(codeOrAccessToken) { var redirectUri = this.buildSuccessRedirectUri(uri, responseType); - this.updateResponse(response, redirectUri, state); + this.updateResponse(response, redirectUri, responseType, state); - return code; + return codeOrAccessToken; }) .catch(function(e) { if (!(e instanceof OAuthError)) { e = new ServerError(e); } - var redirectUri = this.buildErrorRedirectUri(uri, e); - this.updateResponse(response, redirectUri, state); + var redirectUri = this.buildErrorRedirectUri(uri, responseType, e); + + this.updateResponse(response, redirectUri, responseType, 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. */ @@ -171,6 +147,7 @@ AuthorizeHandler.prototype.getClient = function(request) { 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) { @@ -181,7 +158,10 @@ AuthorizeHandler.prototype.getClient = function(request) { throw new InvalidClientError('Invalid client: missing client `grants`'); } - if (!_.includes(client.grants, 'authorization_code')) { + var responseType = request.body.response_type || request.query.response_type; + var requestedGrantType = responseType === 'token' ? 'implicit' : 'authorization_code'; + + if (!_.includes(client.grants, requestedGrantType)) { throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); } @@ -192,10 +172,29 @@ AuthorizeHandler.prototype.getClient = function(request) { if (redirectUri && !_.includes(client.redirectUris, redirectUri)) { throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); } + return client; }); }; +/** + * Validate requested scope. + */ +AuthorizeHandler.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 Promise.resolve(scope); + } +}; + /** * Get scope from the request. */ @@ -253,27 +252,14 @@ 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) { +AuthorizeHandler.prototype.getResponseType = function(request, client) { var responseType = request.body.response_type || request.query.response_type; - + if (!responseType) { throw new InvalidRequestError('Missing parameter: `response_type`'); } @@ -282,6 +268,10 @@ AuthorizeHandler.prototype.getResponseType = function(request) { throw new UnsupportedResponseTypeError('Unsupported response type: `response_type` is not supported'); } + if (responseType === 'token' && (!client || !_.includes(client.grants, 'implicit'))) { + throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); + } + return responseTypes[responseType]; }; @@ -290,22 +280,31 @@ AuthorizeHandler.prototype.getResponseType = function(request) { */ AuthorizeHandler.prototype.buildSuccessRedirectUri = function(redirectUri, responseType) { - return responseType.buildRedirectUri(redirectUri); + var uri = url.parse(redirectUri); + return responseType.buildRedirectUri(uri); }; /** * Build an error response that redirects the user-agent to the client-provided url. */ -AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) { +AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, responseType, error) { var uri = url.parse(redirectUri); - uri.query = { - error: error.name - }; - - if (error.message) { - uri.query.error_description = error.message; + 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; @@ -315,10 +314,11 @@ AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) * 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) { +AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, responseType, state) { + if (responseType && state) { + redirectUri = responseType.setRedirectUriParam(redirectUri, 'state', state); + } else if (state) { + redirectUri.query = redirectUri.query || {}; redirectUri.query.state = state; } diff --git a/lib/handlers/revoke-handler.js b/lib/handlers/revoke-handler.js new file mode 100644 index 000000000..0624b747d --- /dev/null +++ b/lib/handlers/revoke-handler.js @@ -0,0 +1,326 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var InvalidClientError = require('../errors/invalid-client-error'); +var InvalidTokenError = require('../errors/invalid-token-error'); +var InvalidRequestError = require('../errors/invalid-request-error'); +var OAuthError = require('../errors/oauth-error'); +var Promise = require('bluebird'); +var promisify = require('promisify-any'); +var Request = require('../request'); +var Response = require('../response'); +var ServerError = require('../errors/server-error'); +var auth = require('basic-auth'); +var is = require('../validator/is'); + +/** + * Constructor. + */ + +function RevokeHandler(options) { + options = options || {}; + + 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. + */ + +RevokeHandler.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.handleRevokeToken(request, client); + }) + .catch(function(e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + /** + * 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 (!(e instanceof InvalidTokenError)) { + this.updateErrorResponse(response, e); + } + + throw e; + }); +}; + +/** + * 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" + */ + +RevokeHandler.prototype.handleRevokeToken = function(request, client) { + return Promise.bind(this) + .then(function() { + return this.getTokenFromRequest(request); + }) + .then(function(token) { + return Promise.any([ + this.getAccessToken(token, client), + this.getRefreshToken(token, client) + ]) + .catch(Promise.AggregateError, function(err) { + err.forEach(function(e) { + throw e; + }); + }) + .bind(this) + .tap(function(token) { + return this.revokeToken(token); + }); + }); +}; + +/** + * Get the client from the model. + */ + +RevokeHandler.prototype.getClient = function(request, response) { + var 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`'); + } + + return Promise.try(promisify(this.model.getClient, 2), [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 + */ + +RevokeHandler.prototype.getClientCredentials = function(request) { + var credentials = auth(request); + + 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 + */ + +RevokeHandler.prototype.getTokenFromRequest = function(request) { + var bodyToken = request.body.token; + + if (!bodyToken) { + throw new InvalidRequestError('Missing parameter: `token`'); + } + + return bodyToken; +}; + +/** + * Get refresh token. + */ + +RevokeHandler.prototype.getRefreshToken = function(token, client) { + return Promise.try(promisify(this.model.getRefreshToken, 1), token) + .then(function(token) { + if (!token) { + throw new InvalidTokenError('Invalid token: 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 InvalidClientError('Invalid client: client 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 InvalidTokenError('Invalid token: refresh token has expired'); + } + + return token; + }); +}; + +/** + * Get the access token from the model. + */ + +RevokeHandler.prototype.getAccessToken = function(token, client) { + return Promise.try(promisify(this.model.getAccessToken, 1), token) + .then(function(accessToken) { + 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 < new Date()) { + throw new InvalidTokenError('Invalid token: access token has expired.'); + } + + return accessToken; + }); +}; + +/** + * Revoke the token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + +RevokeHandler.prototype.revokeToken = function(token) { + return Promise.try(promisify(this.model.revokeToken, 1), token) + .then(function(token) { + if (!token) { + throw new InvalidTokenError('Invalid token: token is invalid'); + } + + return token; + }); +}; + +/** + * Update response when an error is thrown. + */ + +RevokeHandler.prototype.updateErrorResponse = function(response, error) { + response.body = { + error: error.name, + error_description: error.message + }; + + response.status = error.code; +}; + +/** + * Export constructor. + */ + +module.exports = RevokeHandler; diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index feaad3f54..af162ca38 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -85,6 +85,9 @@ TokenHandler.prototype.handle = function(request, response) { return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')); } + // Extend model object with request + this.model.request = request; + return Promise.bind(this) .then(function() { return this.getClient(request, response); diff --git a/lib/request.js b/lib/request.js index 65e2db303..00b879586 100644 --- a/lib/request.js +++ b/lib/request.js @@ -33,14 +33,14 @@ function Request(options) { // Store the headers in lower case. for (var field in options.headers) { - if (options.headers.hasOwnProperty(field)) { + if (Object.prototype.hasOwnProperty.call(options.headers, 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]) { + if (Object.prototype.hasOwnProperty.call(options, property) && !this[property]) { this[property] = options[property]; } } diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js index 6eaf23a89..f0dd8a141 100644 --- a/lib/response-types/code-response-type.js +++ b/lib/response-types/code-response-type.js @@ -5,20 +5,118 @@ */ var InvalidArgumentError = require('../errors/invalid-argument-error'); -var url = require('url'); +var tokenUtil = require('../utils/token-util'); +var Promise = require('bluebird'); /** * Constructor. */ -function CodeResponseType(code) { - if (!code) { - throw new InvalidArgumentError('Missing parameter: `code`'); +function CodeResponseType(options) { + options = options || {}; + + if (!options.authorizationCodeLifetime) { + throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); } - this.code = code; + if (!options.model.saveAuthorizationCode) { + throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); + } + + this.code = null; + this.authorizationCodeLifetime = options.authorizationCodeLifetime; + this.model = options.model; } +/** + * Handle code response type. + */ + +CodeResponseType.prototype.handle = function(request, client, user, uri, scope) { + 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`'); + } + + var fns = [ + this.generateAuthorizationCode(), + this.getAuthorizationCodeExpiresAt(client) + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(authorizationCode, expiresAt) { + return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user); + }) + .then(function(code) { + this.code = code.authorizationCode; + return code; + }); +}; + +/** + * Get authorization code expiration date. + */ + +CodeResponseType.prototype.getAuthorizationCodeExpiresAt = function(client) { + var expires = new Date(); + var authorizationCodeLifetime = this.getAuthorizationCodeLifetime(client); + + expires.setSeconds(expires.getSeconds() + authorizationCodeLifetime); + + return expires; +}; + +/** + * Get authorization code lifetime. + */ + +CodeResponseType.prototype.getAuthorizationCodeLifetime = function(client) { + return client.authorizationCodeLifetime || this.authorizationCodeLifetime; +}; + +/** + * Save authorization code. + */ + +CodeResponseType.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) { + var code = { + authorizationCode: authorizationCode, + expiresAt: expiresAt, + redirectUri: redirectUri, + scope: scope + }; + + return Promise.try(this.model.saveAuthorizationCode, [code, client, user]); +}; + +/** + * Generate authorization code. + */ + +CodeResponseType.prototype.generateAuthorizationCode = function() { + if (this.model.generateAuthorizationCode) { + return Promise.try(this.model.generateAuthorizationCode); + } + + return tokenUtil.generateRandomToken(); +}; + /** * Build redirect uri. */ @@ -28,12 +126,28 @@ CodeResponseType.prototype.buildRedirectUri = function(redirectUri) { throw new InvalidArgumentError('Missing parameter: `redirectUri`'); } - var uri = url.parse(redirectUri, true); + redirectUri.search = null; + + return this.setRedirectUriParam(redirectUri, 'code', this.code); +}; + +/** + * Set redirect uri parameter. + */ + +CodeResponseType.prototype.setRedirectUriParam = function(redirectUri, key, value) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + if (!key) { + throw new InvalidArgumentError('Missing parameter: `key`'); + } - uri.query.code = this.code; - uri.search = null; + redirectUri.query = redirectUri.query || {}; + redirectUri.query[key] = value; - return uri; + return redirectUri; }; /** diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js index 2637f64cd..92352150e 100644 --- a/lib/response-types/token-response-type.js +++ b/lib/response-types/token-response-type.js @@ -4,16 +4,95 @@ * Module dependencies. */ -var ServerError = require('../errors/server-error'); +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var ImplicitGrantType = require('../grant-types/implicit-grant-type'); +var Promise = require('bluebird'); /** * Constructor. */ -function TokenResponseType() { - throw new ServerError('Not implemented.'); +function TokenResponseType(options) { + options = options || {}; + + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); + } + + this.accessToken = null; + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; } +/** + * Handle token response type. + */ + +TokenResponseType.prototype.handle = function(request, client, user, uri, scope) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + var accessTokenLifetime = this.getAccessTokenLifetime(client); + + var options = { + user: user, + scope: scope, + model: this.model, + accessTokenLifetime: accessTokenLifetime + }; + + var grantType = new ImplicitGrantType(options); + + return Promise.bind(this) + .then(function() { + return grantType.handle(request, client); + }) + .then(function(token) { + this.accessToken = token.accessToken; + return token; + }); +}; + +/** + * Get access token lifetime. + */ + +TokenResponseType.prototype.getAccessTokenLifetime = function(client) { + return client.accessTokenLifetime || this.accessTokenLifetime; +}; + +/** + * Build redirect uri. + */ + +TokenResponseType.prototype.buildRedirectUri = function(redirectUri) { + return this.setRedirectUriParam(redirectUri, 'access_token', this.accessToken); +}; + +/** + * Set redirect uri parameter. + */ + +TokenResponseType.prototype.setRedirectUriParam = function(redirectUri, key, value) { + 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; +}; + /** * Export constructor. */ diff --git a/lib/response.js b/lib/response.js index 4e9d1ec69..76fe854ab 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,14 +13,14 @@ function Response(options) { // Store the headers in lower case. for (var field in options.headers) { - if (options.headers.hasOwnProperty(field)) { + if (Object.prototype.hasOwnProperty.call(options.headers, 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]) { + if (Object.prototype.hasOwnProperty.call(options, property) && !this[property]) { this[property] = options[property]; } } diff --git a/lib/server.js b/lib/server.js index fba9ccf81..3a4901c70 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,6 +9,7 @@ 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'); +var RevokeHandler = require('./handlers/revoke-handler'); /** * Constructor. @@ -51,6 +52,7 @@ OAuth2Server.prototype.authenticate = function(request, response, options, callb OAuth2Server.prototype.authorize = function(request, response, options, callback) { options = _.assign({ allowEmptyState: false, + accessTokenLifetime: 60 * 60, // 1 hour. authorizationCodeLifetime: 5 * 60 // 5 minutes. }, this.options, options); @@ -76,6 +78,18 @@ OAuth2Server.prototype.token = function(request, response, options, callback) { .nodeify(callback); }; +/** + * Revoke a token. + */ + +OAuth2Server.prototype.revoke = function(request, response, options, callback) { + options = _.assign(this.options, options); + + return new RevokeHandler(options) + .handle(request, response) + .nodeify(callback); +}; + /** * Export constructor. */ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..052ce992d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1415 @@ +{ + "name": "oauth2-server", + "version": "4.0.0-dev.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/commons": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz", + "integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, + "@sinonjs/samsam": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.0.0.tgz", + "integrity": "sha512-D7VxhADdZbDJ0HjUTMnSQ5xIGb4H2yWpg8k9Sf1T08zfFiQYlaxM8LZydpR4FQ2E6LZJX8IlabNZ5io4vdChwg==", + "dev": true + }, + "acorn": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz", + "integrity": "sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw==", + "dev": true + }, + "acorn-jsx": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-4.1.1.tgz", + "integrity": "sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw==", + "dev": true, + "requires": { + "acorn": "^5.0.3" + } + }, + "ajv": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", + "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "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-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "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" + } + }, + "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 + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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" + } + } + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "co-bluebird": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/co-bluebird/-/co-bluebird-1.1.0.tgz", + "integrity": "sha1-yLnzqTIKftMJh9zKGlw8/1llXHw=", + "requires": { + "bluebird": "^2.10.0", + "co-use": "^1.1.0" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "co-use": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/co-use/-/co-use-1.1.0.tgz", + "integrity": "sha1-xrs83xDLc17Kqdru2kbXJclKTmI=" + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "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 + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "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 + }, + "eslint": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.4.0.tgz", + "integrity": "sha512-UIpL91XGex3qtL6qwyCQJar2j3osKxK9e3ano3OcGEIRM4oWIpCkDg9x95AXEC2wMs7PnxzOkPZ2gq+tsMS9yg==", + "dev": true, + "requires": { + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.2", + "imurmurhash": "^0.1.4", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.5.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" + } + }, + "eslint-scope": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", + "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-4.0.0.tgz", + "integrity": "sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg==", + "dev": true, + "requires": { + "acorn": "^5.6.0", + "acorn-jsx": "^4.1.1" + } + }, + "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 + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "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" + } + }, + "globals": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", + "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "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-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "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 + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "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-generator": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", + "integrity": "sha1-wUwhBX7TbjKNuANHlmxpP4hjifM=" + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "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 + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "just-extend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz", + "integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "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 + }, + "lolex": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz", + "integrity": "sha512-Oo2Si3RMKV3+lV5MsSWplDQFoTClz/24S0MMHYcgGWWmFXr6TMlqcqk/l1GtH+d5wLBwNRiqGnwDRMirtFalJw==", + "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=" + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "requires": { + "mime-db": "~1.36.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "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": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "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" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "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": "1.4.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.4.tgz", + "integrity": "sha512-pxE0c9PzgrUTyhfv5p+5eMIdfU2bLEsq8VQEuE0kxM4zP7SujSar7rk9wpI2F7RyyCEvLyj5O7Is3RER5F36Fg==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^2.0.0", + "just-extend": "^3.0.0", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0", + "text-encoding": "^0.6.4" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "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-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "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-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "promisify-any": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promisify-any/-/promisify-any-2.0.1.tgz", + "integrity": "sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU=", + "requires": { + "bluebird": "^2.10.0", + "co-bluebird": "^1.1.0", + "is-generator": "^1.0.2" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "regexpp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.0.tgz", + "integrity": "sha512-g2FAVtR8Uh8GO1Nv5wpxW7VFVwHcCEr4wyA8/MHiRkO8uHoR5ntAA8Uq3P1vvMTX/BeQiRVSpDGLd+Wn5HNOTA==", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, + "semver": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "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 + }, + "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 + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sinon": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-6.1.5.tgz", + "integrity": "sha512-TcbRoWs1SdY6NOqfj0c9OEQquBoZH+qEf8799m1jjcbfWrrpyCQ3B/BpX7+NKa7Vn33Jl+Z50H4Oys3bzygK2Q==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.1", + "@sinonjs/formatio": "^2.0.0", + "@sinonjs/samsam": "^2.0.0", + "diff": "^3.5.0", + "lodash.get": "^4.4.2", + "lolex": "^2.7.1", + "nise": "^1.4.2", + "supports-color": "^5.4.0", + "type-detect": "^4.0.8" + }, + "dependencies": { + "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" + } + } + } + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "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" + } + }, + "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" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + }, + "table": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", + "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", + "dev": true, + "requires": { + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "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-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.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" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/package.json b/package.json index 6992ef15e..c4d4828f4 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,70 @@ { "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": "4.0.0-dev.2", "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" + "basic-auth": "^2.0.0", + "bluebird": "^3.5.1", + "lodash": "^4.17.10", + "promisify-any": "^2.0.1", + "statuses": "^1.5.0", + "type-is": "^1.6.16" }, "devDependencies": { - "jshint": "2.9.4", - "mocha": "3.3.0", - "should": "11.2.1", - "sinon": "2.3.2" + "eslint": "^5.2.0", + "mocha": "^5.2.0", + "should": "^13.2.3", + "sinon": "^6.1.4" }, "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=6.14" }, "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'" + "pretest": "eslint lib test --fix", + "test": "NODE_ENV=test mocha 'test/**/*_test.js'", + "test-debug": "NODE_ENV=test mocha --inspect --debug-brk 'test/**/*_test.js'" }, "repository": { "type": "git", diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index 6da489cd9..6e6258b29 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -64,7 +64,7 @@ describe('AbstractGrantType integration', function() { return handler.generateAccessToken() .then(function(data) { - data.should.be.a.sha1; + data.should.be.a.sha1(); }) .catch(should.fail); }); @@ -98,7 +98,7 @@ describe('AbstractGrantType integration', function() { return handler.generateRefreshToken() .then(function(data) { - data.should.be.a.sha1; + data.should.be.a.sha1(); }) .catch(should.fail); }); diff --git a/test/integration/grant-types/implicit-grant-type_test.js b/test/integration/grant-types/implicit-grant-type_test.js new file mode 100644 index 000000000..2498cc591 --- /dev/null +++ b/test/integration/grant-types/implicit-grant-type_test.js @@ -0,0 +1,260 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var ImplicitGrantType = require('../../../lib/grant-types/implicit-grant-type'); +var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +var Promise = require('bluebird'); +var Request = require('../../../lib/request'); +var should = require('should'); + +/** + * Test `ImplicitGrantType` integration. + */ + +describe('ImplicitGrantType integration', function() { + describe('constructor()', function() { + it('should throw an error if `model` is missing', function() { + try { + new ImplicitGrantType(); + + 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()`', function() { + try { + var model = {}; + + new ImplicitGrantType({ model: model }); + + 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', function() { + try { + var model = { + saveToken: function() {} + }; + + new ImplicitGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `user`'); + } + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var model = { + saveToken: function() {} + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + + 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 = { + saveToken: function() {} + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + 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 = { accessToken: 'foobar-token' }; + var model = { + saveToken: function() { return token; }, + validateScope: function() { return 'foo'; } + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + 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 = { + saveToken: function() {} + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + 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 = { + saveToken: function() {} + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + 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 = { + saveToken: function(tokenToSave, client, user, callback) { callback(null, tokenToSave); } + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + var request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {} + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + grantType.handle(request, client).then(function(data) { + data.should.have.keys('accessToken', 'accessTokenExpiresAt'); + data.accessToken.should.be.type('string'); + }); + + }); + }); + + describe('saveToken()', function() { + it('should save the token', function() { + var token = {}; + var model = { + saveToken: function() { return token; }, + validateScope: function() { return 'foo'; } + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + + return grantType.saveToken(token) + .then(function(data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + + it('should support promises', function() { + var token = {}; + var model = { + saveToken: function() { return Promise.resolve(token); } + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var token = {}; + var model = { + saveToken: function() { return token; } + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support callbacks', function() { + var token = {}; + var model = { + saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } + }; + var grantType = new ImplicitGrantType({ + accessTokenLifetime: 123, + model: model, + user: {} + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); +}); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 7852ea2eb..6ed227eaa 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -168,7 +168,7 @@ describe('AuthenticateHandler integration', function() { }); }); - it('should return an access token', function() { + it('should return an access token with extend model obj with request', function() { var accessToken = { user: {}, accessTokenExpiresAt: new Date(new Date().getTime() + 10000) @@ -192,6 +192,7 @@ describe('AuthenticateHandler integration', function() { return handler.handle(request, response) .then(function(data) { + model.request.should.equal(request); data.should.equal(accessToken); }) .catch(should.fail); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 0d1aa333b..0cb8faddc 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -27,17 +27,6 @@ var url = require('url'); 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 }); @@ -60,17 +49,6 @@ describe('AuthorizeHandler integration', function() { } }); - it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: function() {} } }); - - 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() {}, @@ -87,17 +65,6 @@ describe('AuthorizeHandler integration', function() { } }); - 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() {}, @@ -108,17 +75,6 @@ describe('AuthorizeHandler integration', function() { 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() { @@ -293,6 +249,46 @@ describe('AuthorizeHandler integration', function() { .catch(should.fail); }); + + it('given an implicit grant flow, should redirect to a successful response with `token` and `state` if successful', function() { + var client = { grants: ['implicit'], redirectUris: ['http://example.com/cb'] }; + var token = { accessToken: 'foobar-token' } + var model = { + getAccessToken: function() { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) + }; + }, + getClient: function() { + return client; + }, + saveToken: function() { return token; } + }; + var handler = new AuthorizeHandler({ accessTokenLifetime: 120, model: model }); + var request = new Request({ + body: { + }, + headers: { + 'Authorization': 'Bearer foo' + }, + method: {}, + query: { + client_id: 12345, + response_type: 'token', + state: 'foobar' + } + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(function() { + response.get('location').should.equal('http://example.com/cb#access_token=foobar-token&state=foobar'); + }) + .catch(should.fail); + }); + it('should redirect to an error response if `scope` is invalid', function() { var model = { getAccessToken: function() { @@ -332,6 +328,94 @@ describe('AuthorizeHandler integration', function() { }); }); + it('should redirect to a successful response if `model.validateScope` is not defined', 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: { + scope: 'read', + 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); + }); + + it('should redirect to an error response if `scope` is insufficient', 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 }; + }, + validateScope: function() { + return false; + } + }; + 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: 'read', + 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%20scope%3A%20Requested%20scope%20is%20invalid'); + }); + }); + it('should redirect to an error response if `state` is missing', function() { var model = { getAccessToken: function() { @@ -444,7 +528,7 @@ describe('AuthorizeHandler integration', function() { }); }); - it('should return the `code` if successful', function() { + it('should return the `code` if successful with extend model obj with request', function() { var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; var model = { getAccessToken: function() { @@ -479,6 +563,7 @@ describe('AuthorizeHandler integration', function() { return handler.handle(request, response) .then(function(data) { + model.request.should.equal(request); data.should.eql({ authorizationCode: 12345, client: client @@ -488,64 +573,6 @@ describe('AuthorizeHandler integration', function() { }); }); - 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 = { @@ -935,65 +962,6 @@ describe('AuthorizeHandler integration', function() { }); }); - 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 = { @@ -1014,7 +982,7 @@ describe('AuthorizeHandler integration', function() { } }); - it('should throw an error if `response_type` is not `code`', function() { + it('should throw an error if `response_type` is not `code` or `token`', function() { var model = { getAccessToken: function() {}, getClient: function() {}, @@ -1036,10 +1004,10 @@ describe('AuthorizeHandler integration', function() { describe('with `response_type` in the request body', function() { it('should return a response type', function() { var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; + 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); @@ -1051,10 +1019,10 @@ describe('AuthorizeHandler integration', function() { describe('with `response_type` in the request query', function() { it('should return a response type', function() { var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; + 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); @@ -1072,7 +1040,10 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: function() {} }; var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var responseType = new CodeResponseType(12345); + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: { + saveAuthorizationCode: function() {} + }}); + responseType.code = 12345; var redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); @@ -1088,7 +1059,10 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: function() {} }; var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: { + saveAuthorizationCode: function() {} + }}); + var 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'); }); @@ -1101,7 +1075,10 @@ describe('AuthorizeHandler integration', function() { saveAuthorizationCode: function() {} }; var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: { + saveAuthorizationCode: function() {} + }}); + var 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'); }); @@ -1117,8 +1094,11 @@ describe('AuthorizeHandler integration', function() { var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var response = new Response({ body: {}, headers: {} }); var uri = url.parse('http://example.com/cb'); + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: { + saveAuthorizationCode: function() {} + }}); - handler.updateResponse(response, uri, 'foobar'); + handler.updateResponse(response, uri, responseType, 'foobar'); response.get('location').should.equal('http://example.com/cb?state=foobar'); }); diff --git a/test/integration/handlers/revoke-handler_test.js b/test/integration/handlers/revoke-handler_test.js new file mode 100644 index 000000000..aeaf176c9 --- /dev/null +++ b/test/integration/handlers/revoke-handler_test.js @@ -0,0 +1,860 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var AccessDeniedError = require('../../../lib/errors/access-denied-error'); +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 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 RevokeHandler = require('../../../lib/handlers/revoke-handler'); +var should = require('should'); +var util = require('util'); + +/** + * Test `RevokeHandler` integration. + */ + +describe('RevokeHandler integration', function() { + describe('constructor()', function() { + + it('should throw an error if `options.model` is missing', function() { + try { + new RevokeHandler({}); + + 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 RevokeHandler({ model: {} }); + + 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`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ 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 = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ 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 the method is not `POST`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'hash' }, 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 the error if an oauth error is thrown', function() { + var model = { + getClient: function() { return { grants: ['password'] }; }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, 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(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', function() { + var model = { + getClient: function() { + throw new Error('Unhandled exception'); + }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var 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: {} + }); + 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'); + }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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(500); + }); + }); + + it('should not update the response if an invalid token error is thrown', function() { + var token = { refreshToken: 'hash', client: {}, user: {}, refreshTokenExpiresAt: new Date('2015-01-01') }; + var client = { grants: ['password'] }; + var model = { + getClient: function() { return client; }, + revokeToken: function() { return token; }, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var 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: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + response.body.should.eql({}); + response.status.should.equal(200); + }); + }); + + it('should return an empty object if successful', function() { + var token = { refreshToken: 'hash', client: {}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; + var client = { grants: ['password'] }; + var model = { + getClient: function() { return client; }, + revokeToken: function() { return token; }, + getRefreshToken: function() { return token; }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var 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: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(function(data) { + should.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', function() { + it('should throw an error if `clientId` is invalid', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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 `clientId` is invalid', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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 {}; }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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 a 401 error if the client is invalid and the request contains an authorization header', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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; }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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); + }); + + it('should support promises', function() { + var model = { + getClient: function() { return Promise.resolve({ grants: [] }); }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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: [] }); + }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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: [] }; }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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 `client_secret` in the request header as basic auth', function() { + it('should return a client', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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('handleRevokeToken()', function() { + it('should throw an error if `token` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return handler.handleRevokeToken(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should return a token', function() { + var client = { id: 12345, grants: ['password'] }; + var token = { accessToken: 'hash', client: { id: 12345 }, accessTokenExpiresAt: new Date(new Date() * 2), user: {} }; + var model = { + getClient: function() {}, + revokeToken: function() { return token; }, + getRefreshToken: function() {}, + getAccessToken: function() { return token; } + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'hash' }, headers: {}, method: {}, query: {} }); + + return handler.handleRevokeToken(request, client) + .then(function(data) { + should.exist(data); + }) + .catch(should.fail); + }); + + it('should return a token', function() { + var client = { id: 12345, grants: ['password'] }; + var token = { refreshToken: 'hash', client: { id: 12345 }, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; + var model = { + getClient: function() {}, + revokeToken: function() { return token; }, + getRefreshToken: function() { return token; }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'hash' }, headers: {}, method: {}, query: {} }); + + return handler.handleRevokeToken(request, client) + .then(function(data) { + should.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', function() { + it('should throw an error if the `refreshToken` is invalid', function() { + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(should.fail) + .catch(function(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', function() { + var client = { id: 'foo' }; + var token = { refreshToken: 'hash', client: { id: 'baz'}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() { return token; }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should return a token', function() { + var client = { id: 'foo' }; + var token = { refreshToken: 'hash', client: { id: 'foo'}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() { return token; }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + + it('should support callbacks', function() { + var client = { id: 'foo' }; + var token = { refreshToken: 'hash', client: { id: 'foo'}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function(refreshToken, callback) { + callback(null, token); + }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + }); + + describe('getAccessToken()', function() { + it('should throw an error if the `accessToken` is invalid', function() { + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .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 the `client_id` does not match', function() { + var client = { id: 'foo' }; + var token = { accessToken: 'hash', client: { id: 'baz'}, user: {}, accessTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() { return token; }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should return a token', function() { + var client = { id: 'foo' }; + var token = { accessToken: 'hash', client: { id: 'foo'}, user: {}, accessTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() { return token; }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + + it('should support callbacks', function() { + var client = { id: 'foo' }; + var token = { accessToken: 'hash', client: { id: 'foo'}, user: {}, accessTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function(accessToken, callback) { + callback(null, token); + }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + }); + + describe('revokeToken()', function() { + it('should throw an error if the `refreshToken` is invalid', function() { + var token = 'hash'; + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function() { return false; }, + getRefreshToken: function() { return { client: {}, user: {}};}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.revokeToken(token, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: token is invalid'); + }); + }); + + it('should support callbacks', function() { + var token = {}; + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function(tokenObject, callback) { + callback(null, null); + }, + getRefreshToken: function(refreshToken, callback) { + callback(null, { client: {}, user: {}}); + }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.revokeToken(token, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: token is invalid'); + }); + }); + }); + + describe('getTokenFromRequest()', function() { + it('should throw an error if `accessToken` is missing', function() { + + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + try { + handler.getTokenFromRequest(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + } + }); + }); + + describe('updateErrorResponse()', function() { + it('should set the `body`', function() { + var error = new AccessDeniedError('Cannot request a revoke'); + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + 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 revoke'); + }); + + it('should set the `status`', function() { + var error = new AccessDeniedError('Cannot request a revoke'); + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var 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 index 50277c113..f3fd00ab5 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -293,11 +293,11 @@ describe('TokenHandler integration', function() { .then(should.fail) .catch(function() { response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); - response.status.should.equal(503); + response.status.should.equal(500); }); }); - it('should return a bearer token if successful', function() { + it('should return a bearer token if successful with extend model obj with request', function() { var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {} }; var model = { getClient: function() { return { grants: ['password'] }; }, @@ -323,6 +323,7 @@ describe('TokenHandler integration', function() { return handler.handle(request, response) .then(function(data) { + model.request.should.equal(request); data.should.eql(token); }) .catch(should.fail); @@ -542,7 +543,7 @@ describe('TokenHandler integration', function() { requireClientAuthentication: { password: false } - }); + }); var request = new Request({ body: { client_id: 'blah', grant_type: 'password'}, headers: {}, method: {}, query: {} }); return handler.getClient(request) @@ -569,13 +570,13 @@ describe('TokenHandler integration', function() { 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) { diff --git a/test/integration/request_test.js b/test/integration/request_test.js index a43527671..5233e1305 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -76,7 +76,7 @@ describe('Request integration', 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; + (undefined === request.get('content-type')).should.be.true(); }); it('should return the value if the field exists', function() { @@ -147,13 +147,13 @@ describe('Request integration', function() { query: {} }); - request.is('json').should.be.false; + 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; + request.is('text/html').should.be.false(); }); }); }); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index 5461b62c4..19f1d26ae 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -6,7 +6,9 @@ var CodeResponseType = require('../../../lib/response-types/code-response-type'); var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +var Promise = require('bluebird'); var should = require('should'); +var sinon = require('sinon'); var url = require('url'); /** @@ -15,27 +17,53 @@ var url = require('url'); describe('CodeResponseType integration', function() { describe('constructor()', function() { - it('should throw an error if `code` is missing', function() { + it('should throw an error if `options.authorizationCodeLifetime` is missing', function() { try { new CodeResponseType(); should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `code`'); + e.message.should.equal('Missing parameter: `authorizationCodeLifetime`'); } }); it('should set the `code`', function() { - var responseType = new CodeResponseType('foo'); + var model = { + saveAuthorizationCode: function() {} + }; + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); - responseType.code.should.equal('foo'); + responseType.authorizationCodeLifetime.should.equal(120); }); }); + it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { + try { + new CodeResponseType({ authorizationCodeLifetime: 120, model: { } }); + + 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`', function() { + var model = { + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + + handler.authorizationCodeLifetime.should.equal(120); + }); + describe('buildRedirectUri()', function() { it('should throw an error if the `redirectUri` is missing', function() { - var responseType = new CodeResponseType('foo'); + var model = { + saveAuthorizationCode: function() {} + }; + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); try { responseType.buildRedirectUri(); @@ -48,17 +76,177 @@ describe('CodeResponseType integration', function() { }); 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'); + var model = { + saveAuthorizationCode: function() {} + }; + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + responseType.code = 'foo'; + var 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', function() { - var responseType = new CodeResponseType('foo'); - var redirectUri = responseType.buildRedirectUri('http://example.com/cb?foo=bar'); + var model = { + saveAuthorizationCode: function() {} + }; + var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + responseType.code = 'foo'; + var 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`', function() { + var model = { + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + + handler.model.should.equal(model); + }); + + describe('generateAuthorizationCode()', function() { + it('should return an auth code', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ 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 CodeResponseType({ 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 CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); + }); + }); + + describe('getAuthorizationCodeExpiresAt()', function() { + it('should return a date', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + + handler.getAuthorizationCodeExpiresAt({}).should.be.an.instanceOf(Date); + }); + }); + + 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 CodeResponseType({ 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 CodeResponseType({ 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 CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + + handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); + }); + }); + + describe('saveAuthorizationCode()', function() { + it('should call `model.saveAuthorizationCode()`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: sinon.stub().returns({}) + }; + var handler = new CodeResponseType({ 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'); + }) + .catch(should.fail); + }); + }); + + describe('generateAuthorizationCode()', function() { + it('should call `model.generateAuthorizationCode()`', function() { + var model = { + generateAuthorizationCode: sinon.stub().returns({}), + getAccessToken: function() {}, + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); + + return handler.generateAuthorizationCode() + .then(function() { + model.generateAuthorizationCode.callCount.should.equal(1); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/integration/response-types/token-response-type_test.js b/test/integration/response-types/token-response-type_test.js new file mode 100644 index 000000000..7e3cc8b1e --- /dev/null +++ b/test/integration/response-types/token-response-type_test.js @@ -0,0 +1,89 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var TokenResponseType = require('../../../lib/response-types/token-response-type'); +var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +var should = require('should'); +var url = require('url'); + +/** + * Test `TokenResponseType` integration. + */ + +describe('TokenResponseType integration', function() { + describe('constructor()', function() { + it('should throw an error if `options.accessTokenLifetime` is missing', function() { + try { + new TokenResponseType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should set `accessTokenLifetime`', function() { + var responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {} + }); + + responseType.accessTokenLifetime.should.equal(120); + }); + + it('should set the `model`', function() { + var model = { + foobar: function() {} + }; + var handler = new TokenResponseType({ accessTokenLifetime: 120, model: model }); + + handler.model.should.equal(model); + }); + }); + + describe('buildRedirectUri()', function() { + it('should throw an error if the `redirectUri` is missing', function() { + var responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {} + }); + + 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 `access_token` and `state` in the query', function() { + var responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {} + }); + + responseType.accessToken = 'foobar-token'; + var 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', function() { + var responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {} + }); + + responseType.accessToken = 'foobar-token'; + var 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_test.js b/test/integration/response_test.js index 1e1e0206a..821069abf 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -35,7 +35,7 @@ describe('Response integration', 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; + (undefined === response.get('content-type')).should.be.true(); }); it('should return the value if the field exists', function() { diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 2d3aa7845..72b9aa92a 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -52,9 +52,9 @@ describe('Server integration', function() { return server.authenticate(request, response) .then(function() { - this.addAcceptedScopesHeader.should.be.true; - this.addAuthorizedScopesHeader.should.be.true; - this.allowBearerTokensInQueryString.should.be.false; + this.addAcceptedScopesHeader.should.be.true(); + this.addAuthorizedScopesHeader.should.be.true(); + this.allowBearerTokensInQueryString.should.be.false(); }) .catch(should.fail); }); @@ -115,8 +115,7 @@ describe('Server integration', function() { return server.authorize(request, response) .then(function() { - this.allowEmptyState.should.be.false; - this.authorizationCodeLifetime.should.equal(300); + this.allowEmptyState.should.be.false(); }) .catch(should.fail); }); @@ -225,7 +224,7 @@ describe('Server integration', function() { return { accessToken: 1234, client: {}, user: {} }; }, validateScope: function() { - return 'foo'; + return 'foo'; } }; var server = new Server({ model: model }); @@ -235,4 +234,62 @@ describe('Server integration', function() { server.token(request, response, null, next); }); }); + + describe('revoke()', function() { + + it('should return a promise', function() { + var model = { + getClient: function() { + return { id: 1234, grants: ['password'] }; + }, + getRefreshToken: function() { + return { + client: { + id: 1234 + }, + user: {} + }; + }, + getAccessToken: function() { + return null; + }, + revokeToken: function() { + return true; + } + }; + var server = new Server({ model: model }); + var request = new Request({ body: { client_id: 1234, client_secret: 'secret', token: 'hash', token_type_hint: 'refresh_token' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + var handler = server.revoke(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + + it('should support callbacks', function(next) { + var model = { + getClient: function() { + return { id: 1234, grants: ['password'] }; + }, + getRefreshToken: function() { + return { + client: { + id: 1234 + }, + user: {} + }; + }, + getAccessToken: function() { + return null; + }, + revokeToken: function() { + return true; + } + }; + var server = new Server({ model: model }); + var request = new Request({ body: { client_id: 1234, client_secret: 'secret', token: 'hash', token_type_hint: 'refresh_token' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + + server.revoke(request, response, null, next); + }); + }); }); diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js index 3fbca3f65..16cca94fb 100644 --- a/test/integration/utils/token-util_test.js +++ b/test/integration/utils/token-util_test.js @@ -16,7 +16,7 @@ describe('TokenUtil integration', function() { it('should return a sha-1 token', function() { return TokenUtil.generateRandomToken() .then(function(token) { - token.should.be.a.sha1; + token.should.be.a.sha1(); }) .catch(should.fail); }); diff --git a/test/unit/grant-types/implicit-grant-type_test.js b/test/unit/grant-types/implicit-grant-type_test.js new file mode 100644 index 000000000..dc100933c --- /dev/null +++ b/test/unit/grant-types/implicit-grant-type_test.js @@ -0,0 +1,50 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var ImplicitGrantType = require('../../../lib/grant-types/implicit-grant-type'); +var Promise = require('bluebird'); +var sinon = require('sinon'); +var should = require('should'); + +/** + * Test `ImplicitGrantType`. + */ + +describe('ImplicitGrantType', function() { + describe('saveToken()', function() { + it('should call `model.saveToken()`', function() { + var client = {}; + var user = {}; + var model = { + saveToken: sinon.stub().returns(true) + }; + var handler = new ImplicitGrantType({ + accessTokenLifetime: 120, + model: model, + user: user + }); + + sinon.stub(handler, 'validateScope').returns('foobar-scope'); + sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foobar-token')); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns(Promise.resolve('foo-1234')); + + 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: '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/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index 2adac7884..5c89f3d1f 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -6,6 +6,7 @@ var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); var Request = require('../../../lib/request'); +var Response = require('../../../lib/response'); var sinon = require('sinon'); var should = require('should'); var ServerError = require('../../../lib/errors/server-error'); @@ -15,6 +16,39 @@ var ServerError = require('../../../lib/errors/server-error'); */ describe('AuthenticateHandler', function() { + describe('handle()', function() { + it('should extend model object with request context', function() { + var model = { + getAccessToken: sinon.stub().returns({ + user: 'foo', + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) + }), + verifyScope: sinon.stub().returns(true) + }; + + var handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'bar' + }); + + var request = new Request({ + body: {}, + headers: { 'Authorization': 'Bearer foo' }, + method: {}, + query: {} + }); + var response = new Response({}); + + return handler.handle(request, response) + .then(function() { + model.request.should.equal(request); + }) + .catch(should.fail); + }); + }); + describe('getTokenFromRequest()', function() { describe('with bearer token in the request authorization header', function() { it('should call `getTokenFromRequestHeader()`', function() { diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index fe9b6b1d7..b172a8ca4 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -16,20 +16,35 @@ var should = require('should'); */ describe('AuthorizeHandler', function() { - describe('generateAuthorizationCode()', function() { - it('should call `model.generateAuthorizationCode()`', function() { + describe('handle()', function() { + it('should extend model object with request context', function() { var model = { - generateAuthorizationCode: sinon.stub().returns({}), - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} + getClient: sinon.stub().returns({ + grants: ['authorization_code'], + redirectUris: ['/abc'] + }), + saveAuthorizationCode: sinon.stub().returns({ authorizationCode: 'code_abc' }) }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.generateAuthorizationCode() + var handler = new AuthorizeHandler({ + authenticateHandler: { + handle: sinon.stub().returns({ name: 'xyz' }) + }, + authorizationCodeLifetime: 123, + allowEmptyState: true, + model: model + }); + + var request = new Request({ + body: { client_id: '123', response_type: 'code' }, + headers: {}, + method: {}, + query: {} + }); + var response = new Response({}); + + return handler.handle(request, response) .then(function() { - model.generateAuthorizationCode.callCount.should.equal(1); - model.generateAuthorizationCode.firstCall.thisValue.should.equal(model); + model.request.should.equal(request); }) .catch(should.fail); }); @@ -77,26 +92,4 @@ describe('AuthorizeHandler', function() { .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-handler_test.js b/test/unit/handlers/revoke-handler_test.js new file mode 100644 index 000000000..9cc56015d --- /dev/null +++ b/test/unit/handlers/revoke-handler_test.js @@ -0,0 +1,103 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var Request = require('../../../lib/request'); +var RevokeHandler = require('../../../lib/handlers/revoke-handler'); +var sinon = require('sinon'); +var should = require('should'); + +/** + * Test `RevokeHandler`. + */ + +describe('RevokeHandler', function() { + describe('handleRevokeToken()', function() { + it('should call `model.getAccessToken()` and `model.getRefreshToken()`', function() { + var model = { + getClient: function() {}, + revokeToken: sinon.stub().returns( true), + getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }), + getAccessToken: sinon.stub().returns( false) + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'foo' }, headers: {}, method: {}, query: {} }); + var client = {}; + + return handler.handleRevokeToken(request, client) + .then(function() { + 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()', function() { + it('should call `model.getClient()`', function() { + var model = { + getClient: sinon.stub().returns({ grants: ['password'] }), + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ 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.args[1].should.equal('secret'); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', function() { + it('should call `model.getRefreshToken()`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() {}, + getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }) + }; + var handler = new RevokeHandler({ model: model }); + var token = 'hash'; + var client = {}; + + return handler.getRefreshToken(token, client) + .then(function() { + 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()', function() { + it('should call `model.revokeToken()`', function() { + var model = { + getClient: function() {}, + revokeToken: sinon.stub().returns( true), + getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }), + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var token = 'hash'; + + return handler.revokeToken(token) + .then(function() { + 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_test.js b/test/unit/handlers/token-handler_test.js index 2b37cd05a..1a0f7c735 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -5,6 +5,7 @@ */ var Request = require('../../../lib/request'); +var Response = require('../../../lib/response'); var TokenHandler = require('../../../lib/handlers/token-handler'); var sinon = require('sinon'); var should = require('should'); @@ -14,6 +15,42 @@ var should = require('should'); */ describe('TokenHandler', function() { + describe('handle()', function() { + it('should extend model object with request context', function() { + var model = { + getClient: sinon.stub().returns({ grants: ['client_credentials'] }), + getUserFromClient: sinon.stub().returns({}), + saveToken: sinon.stub().returns({ + accessToken: '123', + client: {}, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + refreshTokenExpiresAt: new Date(new Date().getTime() + 10000) + }), + }; + + var handler = new TokenHandler({ + accessTokenLifetime: 123, + refreshTokenLifetime: 123, + model: model, + }); + + var request = new Request({ + method: 'POST', + body: { 'grant_type': 'client_credentials', 'client_id': 'abc', 'client_secret': 'xyz' }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + query: {} + }); + var response = new Response({}); + + return handler.handle(request, response) + .then(function() { + model.request.should.equal(request); + }) + .catch(should.fail); + }); + }); + describe('getClient()', function() { it('should call `model.getClient()`', function() { var model = { diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 3d899951c..5aad98ca8 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -11,14 +11,14 @@ describe('Model', function() { atExpiresAt.setHours(new Date().getHours() + 1); var data = { - accessToken: 'foo', - client: 'bar', - user: 'tar', - accessTokenExpiresAt: atExpiresAt + accessToken: 'foo', + client: 'bar', + user: 'tar', + accessTokenExpiresAt: atExpiresAt }; var model = new TokenModel(data); - model.accessTokenLifetime.should.be.Number; + model.accessTokenLifetime.should.be.Number(); model.accessTokenLifetime.should.be.approximately(3600, 2); }); }); diff --git a/test/unit/response_test.js b/test/unit/response_test.js index c435e32f7..d226df673 100644 --- a/test/unit/response_test.js +++ b/test/unit/response_test.js @@ -107,14 +107,14 @@ describe('Request', function() { 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); - }); + 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_test.js b/test/unit/server_test.js index e7c343f0c..21083bef6 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -9,6 +9,7 @@ var AuthorizeHandler = require('../../lib/handlers/authorize-handler'); var Promise = require('bluebird'); var Server = require('../../lib/server'); var TokenHandler = require('../../lib/handlers/token-handler'); +var RevokeHandler = require('../../lib/handlers/revoke-handler'); var sinon = require('sinon'); /** @@ -87,4 +88,24 @@ describe('Server', function() { TokenHandler.prototype.handle.restore(); }); }); + + describe('revoke()', function() { + it('should call `handle`', function() { + var model = { + getClient: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {}, + revokeToken: function() {} + }; + var server = new Server({ model: model }); + + sinon.stub(RevokeHandler.prototype, 'handle').returns(Promise.resolve()); + + server.revoke('foo', 'bar'); + + RevokeHandler.prototype.handle.callCount.should.equal(1); + RevokeHandler.prototype.handle.firstCall.args[0].should.equal('foo'); + RevokeHandler.prototype.handle.restore(); + }); + }); });