diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..7a7bb1570 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a57aeddc4..000000000 --- a/.eslintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "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 old mode 100644 new mode 100755 index 08b44961f..ed37ca5c4 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,19 @@ -node_modules/ + +# IDE +/.idea +/.awcache +/.vscode/*.code-workspace +/.vscode/settings.json + +# misc +npm-debug.log +# folders +/dist +/node_modules docs/_build/ __pycache__/ -*.pyc -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz -*.iml -.idea -.jshint +# files .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 +*.pyc +/aio diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 65e3ba2ed..000000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test/ diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 000000000..6de9cff5b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "lf" +} diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 index 045099f9c..9d70f4ecd --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - - 6 - - 7 - 8 - - 9 - 10 + - 12 sudo: false diff --git a/.vscode/README.md b/.vscode/README.md new file mode 100644 index 000000000..9656796b7 --- /dev/null +++ b/.vscode/README.md @@ -0,0 +1,22 @@ +# VSCode Configuration + +This folder contains opt-in [Workspace Settings](https://code.visualstudio.com/docs/getstarted/settings) and [Extension Recommendations](https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions) that our team recommends using when working on this repository. + +## Usage + +To use the recommended settings follow the steps below: + +- copy `.vscode/recommended-settings.json` to `.vscode/settings.json` +- restart the editor + +If you already have your custom workspace settings you should instead manually merge the file content. + +This isn't an automatic process so you will need to repeat it when settings are updated. + +To see the recommended extensions select "Extensions: Show Recommended Extensions" in the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette). + +## Editing `.vscode/recommended-settings.json` + +If you wish to add extra configuration items please keep in mind any settings you add here will be used by many users. + +Try to keep these settings to things that help facilitate the development process and avoid altering the user workflow whenever possible. diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..7fac8f753 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "ms-vscode.vscode-typescript-tslint-plugin" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..51fea6b85 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "--require", + "ts-node/register", + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "--recursive", + "${workspaceFolder}/test/**/*.spec.ts" + ], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} diff --git a/.vscode/recommended-settings.json b/.vscode/recommended-settings.json new file mode 100644 index 000000000..3fa8122cd --- /dev/null +++ b/.vscode/recommended-settings.json @@ -0,0 +1,39 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/node_modules": true, + "dist": true + }, + "files.watcherExclude": { + "**/.git/**": true, + "**/node_modules/**": true, + "**/dist/**": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true + }, + "editor.formatOnSave": false, + "typescript.format.enable": false, + "editor.formatOnPaste": false, + "[typescript]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll.tslint": true + } + }, + "[json]": { + "editor.formatOnSave": true + }, + "[jsonc]": { + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.formatOnSave": true + }, + "prettier.singleQuote": true, + "prettier.trailingComma": "all", + "prettier.printWidth": 80 +} diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index a9e7df8ec..d24eeea71 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Changelog +### 5.0.0 +* BREAKING: Remove support for node v6 +* BREAKING: Remove support for callbacks only support native Promises +* new: Rewrote in TypeScript +* new: Switch from Eslint to Tslint +* new: added .vscode folder for recommend extensions and recommend setting required for development +* new: added suitable TypeScript Interfaces for various Objects + ### 4.0.0 * BREAKING: Set server_error Code to 500 * BREAKING: Remove support for node v4 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/TODO b/TODO new file mode 100644 index 000000000..723eeaabc --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ + +Todo: + ✔ Add a todo + ✔ A Basic Rewrite of library in TypeScript + ✔ A Basic Rewrite of tests in TypeScript + ☐ Add examples for nestjs, expressjs, koa and others + ☐ Add Migration guide + ☐ Review all Docs diff --git a/index.js b/index.js deleted file mode 100644 index f4f940ab5..000000000 --- a/index.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -/** - * Expose server and request/response classes. - */ - -exports = module.exports = require('./lib/server'); -exports.Request = require('./lib/request'); -exports.Response = require('./lib/response'); - -/** - * Export helpers for extension grants. - */ - -exports.AbstractGrantType = require('./lib/grant-types/abstract-grant-type'); - -/** - * Export error classes. - */ - -exports.AccessDeniedError = require('./lib/errors/access-denied-error'); -exports.InsufficientScopeError = require('./lib/errors/insufficient-scope-error'); -exports.InvalidArgumentError = require('./lib/errors/invalid-argument-error'); -exports.InvalidClientError = require('./lib/errors/invalid-client-error'); -exports.InvalidGrantError = require('./lib/errors/invalid-grant-error'); -exports.InvalidRequestError = require('./lib/errors/invalid-request-error'); -exports.InvalidScopeError = require('./lib/errors/invalid-scope-error'); -exports.InvalidTokenError = require('./lib/errors/invalid-token-error'); -exports.OAuthError = require('./lib/errors/oauth-error'); -exports.ServerError = require('./lib/errors/server-error'); -exports.UnauthorizedClientError = require('./lib/errors/unauthorized-client-error'); -exports.UnauthorizedRequestError = require('./lib/errors/unauthorized-request-error'); -exports.UnsupportedGrantTypeError = require('./lib/errors/unsupported-grant-type-error'); -exports.UnsupportedResponseTypeError = require('./lib/errors/unsupported-response-type-error'); - diff --git a/index.ts b/index.ts new file mode 100755 index 000000000..3cea1819c --- /dev/null +++ b/index.ts @@ -0,0 +1,10 @@ +export * from './lib/errors'; +export * from './lib/grant-types'; +export * from './lib/handlers'; +export * from './lib/interfaces'; +export { Request } from './lib/request'; +export { Response } from './lib/response'; +export * from './lib/response-types'; +export { OAuth2Server } from './lib/server'; +export * from './lib/token-types'; +export * from './lib/validator/is'; diff --git a/lib/constants/common.ts b/lib/constants/common.ts new file mode 100644 index 000000000..d8caa68f2 --- /dev/null +++ b/lib/constants/common.ts @@ -0,0 +1,12 @@ +export const MILLISECONDS_PER_SECOND = 1_000; +export const SECONDS_PER_MINUTE = 60; +export const MINUTES_PER_HOUR = 60; +export const HOURS_PER_DAY = 24; +export const DAYS_PER_WEEK = 7; +export const MONTHS_PER_YEAR = 12; + +export const SECOND = MILLISECONDS_PER_SECOND; +export const MINUTE = SECONDS_PER_MINUTE * SECOND; +export const HOUR = MINUTES_PER_HOUR * MINUTE; +export const DAY = HOURS_PER_DAY * HOUR; +export const WEEK = DAYS_PER_WEEK * DAY; diff --git a/lib/constants/index.ts b/lib/constants/index.ts new file mode 100644 index 000000000..d0b932366 --- /dev/null +++ b/lib/constants/index.ts @@ -0,0 +1 @@ +export * from './common'; diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js deleted file mode 100644 index d3ffc704a..000000000 --- a/lib/errors/access-denied-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The resource owner or authorization server denied the request" - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function AccessDeniedError(message, properties) { - properties = _.assign({ - code: 400, - name: 'access_denied' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(AccessDeniedError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = AccessDeniedError; diff --git a/lib/errors/access-denied-error.ts b/lib/errors/access-denied-error.ts new file mode 100755 index 000000000..d78e98d00 --- /dev/null +++ b/lib/errors/access-denied-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The resource owner or authorization server denied the request" + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class AccessDeniedError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'access_denied', ...properties }); + } +} diff --git a/lib/errors/index.ts b/lib/errors/index.ts new file mode 100644 index 000000000..24141cb6f --- /dev/null +++ b/lib/errors/index.ts @@ -0,0 +1,14 @@ +export { AccessDeniedError } from './access-denied-error'; +export { InsufficientScopeError } from './insufficient-scope-error'; +export { InvalidArgumentError } from './invalid-argument-error'; +export { InvalidClientError } from './invalid-client-error'; +export { InvalidGrantError } from './invalid-grant-error'; +export { InvalidRequestError } from './invalid-request-error'; +export { InvalidScopeError } from './invalid-scope-error'; +export { InvalidTokenError } from './invalid-token-error'; +export { OAuthError } from './oauth-error'; +export { ServerError } from './server-error'; +export { UnauthorizedClientError } from './unauthorized-client-error'; +export { UnauthorizedRequestError } from './unauthorized-request-error'; +export { UnsupportedGrantTypeError } from './unsupported-grant-type-error'; +export { UnsupportedResponseTypeError } from './unsupported-response-type-error'; diff --git a/lib/errors/insufficient-scope-error.js b/lib/errors/insufficient-scope-error.js deleted file mode 100644 index c6442eac6..000000000 --- a/lib/errors/insufficient-scope-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The request requires higher privileges than provided by the access token.." - * - * @see https://tools.ietf.org/html/rfc6750.html#section-3.1 - */ - -function InsufficientScopeError(message, properties) { - properties = _.assign({ - code: 403, - name: 'insufficient_scope' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InsufficientScopeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InsufficientScopeError; diff --git a/lib/errors/insufficient-scope-error.ts b/lib/errors/insufficient-scope-error.ts new file mode 100755 index 000000000..c63720a62 --- /dev/null +++ b/lib/errors/insufficient-scope-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The request requires higher privileges than provided by the access token." + * + * @see https://tools.ietf.org/html/rfc6750.html#section-3.1 + */ + +export class InsufficientScopeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 403, name: 'insufficient_scope', ...properties }); + } +} diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js deleted file mode 100644 index cb56d5a8b..000000000 --- a/lib/errors/invalid-argument-error.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - */ - -function InvalidArgumentError(message, properties) { - properties = _.assign({ - code: 500, - name: 'invalid_argument' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidArgumentError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidArgumentError; diff --git a/lib/errors/invalid-argument-error.ts b/lib/errors/invalid-argument-error.ts new file mode 100755 index 000000000..393dee964 --- /dev/null +++ b/lib/errors/invalid-argument-error.ts @@ -0,0 +1,12 @@ +import { OAuthError } from './oauth-error'; +/** + * Constructor. + * + * "The request requires valid argument." + * + */ +export class InvalidArgumentError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 500, name: 'invalid_argument', ...properties }); + } +} diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js deleted file mode 100644 index d95358c7c..000000000 --- a/lib/errors/invalid-client-error.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "Client authentication failed (e.g., unknown client, no client - * authentication included, or unsupported authentication method)" - * - * @see https://tools.ietf.org/html/rfc6749#section-5.2 - */ - -function InvalidClientError(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_client' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidClientError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidClientError; diff --git a/lib/errors/invalid-client-error.ts b/lib/errors/invalid-client-error.ts new file mode 100755 index 000000000..1b097b046 --- /dev/null +++ b/lib/errors/invalid-client-error.ts @@ -0,0 +1,16 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "Client authentication failed (e.g., unknown client, no client + * authentication included, or unsupported authentication method)" + * + * @see https://tools.ietf.org/html/rfc6749#section-5.2 + */ + +export class InvalidClientError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_client', ...properties }); + } +} diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js deleted file mode 100644 index 58d032e11..000000000 --- a/lib/errors/invalid-grant-error.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The provided authorization grant (e.g., authorization code, resource owner credentials) - * or refresh token is invalid, expired, revoked, does not match the redirection URI used - * in the authorization request, or was issued to another client." - * - * @see https://tools.ietf.org/html/rfc6749#section-5.2 - */ - -function InvalidGrantError(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_grant' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidGrantError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidGrantError; diff --git a/lib/errors/invalid-grant-error.ts b/lib/errors/invalid-grant-error.ts new file mode 100755 index 000000000..046ef1902 --- /dev/null +++ b/lib/errors/invalid-grant-error.ts @@ -0,0 +1,17 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The provided authorization grant (e.g., authorization code, resource owner credentials) + * or refresh token is invalid, expired, revoked, does not match the redirection URI used + * in the authorization request, or was issued to another client." + * + * @see https://tools.ietf.org/html/rfc6749#section-5.2 + */ + +export class InvalidGrantError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_grant', ...properties }); + } +} diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js deleted file mode 100644 index 4cf0a73b1..000000000 --- a/lib/errors/invalid-request-error.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The request is missing a required parameter, includes an invalid parameter value, - * includes a parameter more than once, or is otherwise malformed." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.2.2.1 - */ - -function InvalidRequest(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_request' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidRequest, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidRequest; diff --git a/lib/errors/invalid-request-error.ts b/lib/errors/invalid-request-error.ts new file mode 100755 index 000000000..d77b8683b --- /dev/null +++ b/lib/errors/invalid-request-error.ts @@ -0,0 +1,16 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The request is missing a required parameter, includes an invalid parameter value, + * includes a parameter more than once, or is otherwise malformed." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.2.2.1 + */ + +export class InvalidRequestError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_request', ...properties }); + } +} diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js deleted file mode 100644 index c3b287fc5..000000000 --- a/lib/errors/invalid-scope-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The requested scope is invalid, unknown, or malformed." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function InvalidScopeError(message, properties) { - properties = _.assign({ - code: 400, - name: 'invalid_scope' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidScopeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidScopeError; diff --git a/lib/errors/invalid-scope-error.ts b/lib/errors/invalid-scope-error.ts new file mode 100755 index 000000000..9611d3aac --- /dev/null +++ b/lib/errors/invalid-scope-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The requested scope is invalid, unknown, or malformed." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class InvalidScopeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'invalid_scope', ...properties }); + } +} diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js deleted file mode 100644 index d7e7a8bfe..000000000 --- a/lib/errors/invalid-token-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The access token provided is expired, revoked, malformed, or invalid for other reasons." - * - * @see https://tools.ietf.org/html/rfc6750#section-3.1 - */ - -function InvalidTokenError(message, properties) { - properties = _.assign({ - code: 401, - name: 'invalid_token' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(InvalidTokenError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = InvalidTokenError; diff --git a/lib/errors/invalid-token-error.ts b/lib/errors/invalid-token-error.ts new file mode 100755 index 000000000..13f8a97df --- /dev/null +++ b/lib/errors/invalid-token-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor.invalid_token + * + * "The access token provided is expired, revoked, malformed, or invalid for other reasons." + * + * @see https://tools.ietf.org/html/rfc6750#section-3.1 + */ + +export class InvalidTokenError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 401, name: 'invalid_token', ...properties }); + } +} diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js deleted file mode 100644 index cd7d33930..000000000 --- a/lib/errors/oauth-error.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'); -var util = require('util'); -var statuses = require('statuses'); -/** - * Constructor. - */ - -function OAuthError(messageOrError, properties) { - var message = messageOrError instanceof Error ? messageOrError.message : messageOrError; - var error = messageOrError instanceof Error ? messageOrError : null; - if (_.isEmpty(properties)) - { - properties = {}; - } - - _.defaults(properties, { code: 500 }); - - if (error) { - properties.inner = error; - } - if (_.isEmpty(message)) { - message = statuses[properties.code]; - } - this.code = this.status = this.statusCode = properties.code; - this.message = message; - for (var key in properties) { - if (key !== 'code') { - this[key] = properties[key]; - } - } - Error.captureStackTrace(this, OAuthError); -} - -util.inherits(OAuthError, Error); - -/** - * Export constructor. - */ - -module.exports = OAuthError; diff --git a/lib/errors/oauth-error.ts b/lib/errors/oauth-error.ts new file mode 100755 index 000000000..3fd32de5c --- /dev/null +++ b/lib/errors/oauth-error.ts @@ -0,0 +1,32 @@ +import * as statuses from 'statuses'; + +export class OAuthError extends Error { + code: any; + status: any; + statusCode: any; + constructor(messageOrError: string | Error, properties: any = {}) { + super(); + let message = + messageOrError instanceof Error ? messageOrError.message : messageOrError; + const error = messageOrError instanceof Error ? messageOrError : undefined; + let props: any = {}; + props = properties; + props.code = props.code || 500; // default code 500 + + if (error) { + props.inner = error; + } + if (!message) { + message = statuses[props.code]; + } + this.code = this.status = this.statusCode = props.code; + this.message = message; + + const ignoreAttr = ['code', 'message']; + Object.keys(props) + .filter(key => !ignoreAttr.includes(key)) + .forEach(key => (this[key] = props[key])); + + Error.captureStackTrace(this, OAuthError); + } +} diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js deleted file mode 100644 index 0a2bcf1f0..000000000 --- a/lib/errors/server-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function ServerError(message, properties) { - properties = _.assign({ - code: 500, - name: 'server_error' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(ServerError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = ServerError; diff --git a/lib/errors/server-error.ts b/lib/errors/server-error.ts new file mode 100755 index 000000000..bccc22a41 --- /dev/null +++ b/lib/errors/server-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * ServerError + * + * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class ServerError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 500, name: 'server_error', ...properties }); + } +} diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js deleted file mode 100644 index c05075d3d..000000000 --- a/lib/errors/unauthorized-client-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authenticated client is not authorized to use this authorization grant type." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function UnauthorizedClientError(message, properties) { - properties = _.assign({ - code: 400, - name: 'unauthorized_client' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnauthorizedClientError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnauthorizedClientError; diff --git a/lib/errors/unauthorized-client-error.ts b/lib/errors/unauthorized-client-error.ts new file mode 100755 index 000000000..14f3fa0ae --- /dev/null +++ b/lib/errors/unauthorized-client-error.ts @@ -0,0 +1,15 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The authenticated client is not authorized to use this authorization grant type." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class UnauthorizedClientError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 400, name: 'unauthorized_client', ...properties }); + } +} diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js deleted file mode 100644 index ae7500d9c..000000000 --- a/lib/errors/unauthorized-request-error.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "If the request lacks any authentication information (e.g., the client - * was unaware that authentication is necessary or attempted using an - * unsupported authentication method), the resource server SHOULD NOT - * include an error code or other error information." - * - * @see https://tools.ietf.org/html/rfc6750#section-3.1 - */ - -function UnauthorizedRequestError(message, properties) { - properties = _.assign({ - code: 401, - name: 'unauthorized_request' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnauthorizedRequestError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnauthorizedRequestError; diff --git a/lib/errors/unauthorized-request-error.ts b/lib/errors/unauthorized-request-error.ts new file mode 100755 index 000000000..2de582c43 --- /dev/null +++ b/lib/errors/unauthorized-request-error.ts @@ -0,0 +1,18 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "If the request lacks any authentication information (e.g., the client + * was unaware that authentication is necessary or attempted using an + * unsupported authentication method), the resource server SHOULD NOT + * include an error code or other error information." + * + * @see https://tools.ietf.org/html/rfc6750#section-3.1 + */ + +export class UnauthorizedRequestError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { code: 401, name: 'unauthorized_request', ...properties }); + } +} diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js deleted file mode 100644 index 28ca0ec04..000000000 --- a/lib/errors/unsupported-grant-type-error.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authorization grant type is not supported by the authorization server." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function UnsupportedGrantTypeError(message, properties) { - properties = _.assign({ - code: 400, - name: 'unsupported_grant_type' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnsupportedGrantTypeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnsupportedGrantTypeError; diff --git a/lib/errors/unsupported-grant-type-error.ts b/lib/errors/unsupported-grant-type-error.ts new file mode 100755 index 000000000..5174c6bce --- /dev/null +++ b/lib/errors/unsupported-grant-type-error.ts @@ -0,0 +1,19 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The authorization grant type is not supported by the authorization server." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class UnsupportedGrantTypeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { + code: 400, + name: 'unsupported_grant_type', + ...properties, + }); + } +} diff --git a/lib/errors/unsupported-response-type-error.js b/lib/errors/unsupported-response-type-error.js deleted file mode 100644 index 523cc449b..000000000 --- a/lib/errors/unsupported-response-type-error.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var OAuthError = require('./oauth-error'); -var util = require('util'); - -/** - * Constructor. - * - * "The authorization server does not supported obtaining an - * authorization code using this method." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - */ - -function UnsupportedResponseTypeError(message, properties) { - properties = _.assign({ - code: 400, - name: 'unsupported_response_type' - }, properties); - - OAuthError.call(this, message, properties); -} - -/** - * Inherit prototype. - */ - -util.inherits(UnsupportedResponseTypeError, OAuthError); - -/** - * Export constructor. - */ - -module.exports = UnsupportedResponseTypeError; diff --git a/lib/errors/unsupported-response-type-error.ts b/lib/errors/unsupported-response-type-error.ts new file mode 100755 index 000000000..9e7cd1471 --- /dev/null +++ b/lib/errors/unsupported-response-type-error.ts @@ -0,0 +1,20 @@ +import { OAuthError } from './oauth-error'; + +/** + * Constructor. + * + * "The authorization server does not supported obtaining an + * authorization code using this method." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ + +export class UnsupportedResponseTypeError extends OAuthError { + constructor(message: string | Error = '', properties?: any) { + super(message, { + code: 400, + name: 'unsupported_response_type', + ...properties, + }); + } +} diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js deleted file mode 100644 index 224a473e3..000000000 --- a/lib/grant-types/abstract-grant-type.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidScopeError = require('../errors/invalid-scope-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var is = require('../validator/is'); -var tokenUtil = require('../utils/token-util'); - -/** - * Constructor. - */ - -function AbstractGrantType(options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; -} - -/** - * Generate access token. - */ - -AbstractGrantType.prototype.generateAccessToken = function(client, user, scope) { - if (this.model.generateAccessToken) { - return promisify(this.model.generateAccessToken, 3).call(this.model, client, user, scope) - .then(function(accessToken) { - return accessToken || tokenUtil.generateRandomToken(); - }); - } - - return tokenUtil.generateRandomToken(); -}; - -/** - * Generate refresh token. - */ - -AbstractGrantType.prototype.generateRefreshToken = function(client, user, scope) { - if (this.model.generateRefreshToken) { - return promisify(this.model.generateRefreshToken, 3).call(this.model, client, user, scope) - .then(function(refreshToken) { - return refreshToken || tokenUtil.generateRandomToken(); - }); - } - - return tokenUtil.generateRandomToken(); -}; - -/** - * Get access token expiration date. - */ - -AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { - return new Date(Date.now() + this.accessTokenLifetime * 1000); -}; - -/** - * Get refresh token expiration date. - */ - -AbstractGrantType.prototype.getRefreshTokenExpiresAt = function() { - return new Date(Date.now() + this.refreshTokenLifetime * 1000); -}; - -/** - * Get scope from the request body. - */ - -AbstractGrantType.prototype.getScope = function(request) { - if (!is.nqschar(request.body.scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); - } - - return request.body.scope; -}; - -/** - * Validate requested scope. - */ -AbstractGrantType.prototype.validateScope = function(user, client, scope) { - if (this.model.validateScope) { - return promisify(this.model.validateScope, 3).call(this.model, user, client, scope) - .then(function (scope) { - if (!scope) { - throw new InvalidScopeError('Invalid scope: Requested scope is invalid'); - } - - return scope; - }); - } else { - return scope; - } -}; - -/** - * Export constructor. - */ - -module.exports = AbstractGrantType; diff --git a/lib/grant-types/abstract-grant-type.ts b/lib/grant-types/abstract-grant-type.ts new file mode 100755 index 000000000..8d9adea38 --- /dev/null +++ b/lib/grant-types/abstract-grant-type.ts @@ -0,0 +1,112 @@ +import { MILLISECONDS_PER_SECOND } from '../constants'; +import { InvalidArgumentError, InvalidScopeError } from '../errors'; +import { Client, Model, User } from '../interfaces'; +import { Request } from '../request'; +import * as tokenUtil from '../utils/token-util'; +import * as is from '../validator/is'; + +export class AbstractGrantType { + accessTokenLifetime: number; + model: Model; + refreshTokenLifetime: number; + alwaysIssueNewRefreshToken: boolean; + + constructor(options: any = {}) { + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; + } + + /** + * Generate access token. + */ + + async generateAccessToken(client?: Client, user?: User, scope?: string) { + if (this.model.generateAccessToken) { + const token = await this.model.generateAccessToken(client, user, scope); + + return token ? token : tokenUtil.GenerateRandomToken(); + } + + return tokenUtil.GenerateRandomToken(); + } + + /** + * Generate refresh token. + */ + + async generateRefreshToken(client?: Client, user?: User, scope?: string) { + if (this.model.generateRefreshToken) { + const token = await this.model.generateRefreshToken(client, user, scope); + + return token ? token : tokenUtil.GenerateRandomToken(); + } + + return tokenUtil.GenerateRandomToken(); + } + + /** + * Get access token expiration date. + */ + + getAccessTokenExpiresAt() { + return new Date( + Date.now() + this.accessTokenLifetime * MILLISECONDS_PER_SECOND, + ); + } + + /** + * Get refresh token expiration date. + */ + + getRefreshTokenExpiresAt() { + return new Date( + Date.now() + this.refreshTokenLifetime * MILLISECONDS_PER_SECOND, + ); + } + + /** + * Get scope from the request body. + */ + + getScope(request: Request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + + return request.body.scope; + } + + /** + * Validate requested scope. + */ + async validateScope(user: User, client: Client, scope: string) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + if (!validatedScope) { + throw new InvalidScopeError( + 'Invalid scope: Requested scope is invalid', + ); + } + + return validatedScope; + } + + return scope; + } +} diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js deleted file mode 100644 index 97c126793..000000000 --- a/lib/grant-types/authorization-code-grant-type.js +++ /dev/null @@ -1,206 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var ServerError = require('../errors/server-error'); -var is = require('../validator/is'); -var util = require('util'); - -/** - * Constructor. - */ - -function AuthorizationCodeGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthorizationCode()`'); - } - - if (!options.model.revokeAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthorizationCode()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(AuthorizationCodeGrantType, AbstractGrantType); - -/** - * Handle authorization code grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - */ - -AuthorizationCodeGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - return Promise.bind(this) - .then(function() { - return this.getAuthorizationCode(request, client); - }) - .tap(function(code) { - return this.validateRedirectUri(request, code); - }) - .tap(function(code) { - return this.revokeAuthorizationCode(code); - }) - .then(function(code) { - return this.saveToken(code.user, client, code.authorizationCode, code.scope); - }); -}; - -/** - * Get the authorization code. - */ - -AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, client) { - if (!request.body.code) { - throw new InvalidRequestError('Missing parameter: `code`'); - } - - if (!is.vschar(request.body.code)) { - throw new InvalidRequestError('Invalid parameter: `code`'); - } - return promisify(this.model.getAuthorizationCode, 1).call(this.model, request.body.code) - .then(function(code) { - if (!code) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - if (!code.client) { - throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object'); - } - - if (!code.user) { - throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object'); - } - - if (code.client.id !== client.id) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - if (!(code.expiresAt instanceof Date)) { - throw new ServerError('Server error: `expiresAt` must be a Date instance'); - } - - if (code.expiresAt < new Date()) { - throw new InvalidGrantError('Invalid grant: authorization code has expired'); - } - - if (code.redirectUri && !is.uri(code.redirectUri)) { - throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI'); - } - - return code; - }); -}; - -/** - * Validate the redirect URI. - * - * "The authorization server MUST ensure that the redirect_uri parameter is - * present if the redirect_uri parameter was included in the initial - * authorization request as described in Section 4.1.1, and if included - * ensure that their values are identical." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - */ - -AuthorizationCodeGrantType.prototype.validateRedirectUri = function(request, code) { - if (!code.redirectUri) { - return; - } - - var redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (!is.uri(redirectUri)) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); - } - - if (redirectUri !== code.redirectUri) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is invalid'); - } -}; - -/** - * Revoke the authorization code. - * - * "The authorization code MUST expire shortly after it is issued to mitigate - * the risk of leaks. [...] If an authorization code is used more than once, - * the authorization server MUST deny the request." - * - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 - */ - -AuthorizationCodeGrantType.prototype.revokeAuthorizationCode = function(code) { - return promisify(this.model.revokeAuthorizationCode, 1).call(this.model, code) - .then(function(status) { - if (!status) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - return code; - }); -}; - -/** - * Save token. - */ - -AuthorizationCodeGrantType.prototype.saveToken = function(user, client, authorizationCode, scope) { - var fns = [ - this.validateScope(user, client, scope), - this.generateAccessToken(client, user, scope), - this.generateRefreshToken(client, user, scope), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(scope, accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { - var token = { - accessToken: accessToken, - authorizationCode: authorizationCode, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, - scope: scope - }; - - return promisify(this.model.saveToken, 3).call(this.model, token, client, user); - }); -}; - -/** - * Export constructor. - */ - -module.exports = AuthorizationCodeGrantType; diff --git a/lib/grant-types/authorization-code-grant-type.ts b/lib/grant-types/authorization-code-grant-type.ts new file mode 100755 index 000000000..f00c82c71 --- /dev/null +++ b/lib/grant-types/authorization-code-grant-type.ts @@ -0,0 +1,202 @@ +import { AbstractGrantType } from '.'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../errors'; +import { AuthorizationCode, Client, Token, User } from '../interfaces'; +import { Request } from '../request'; +import * as is from '../validator/is'; + +export class AuthorizationCodeGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAuthorizationCode()`', + ); + } + + if (!options.model.revokeAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeAuthorizationCode()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Handle authorization code grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + const code = await this.getAuthorizationCode(request, client); + this.validateRedirectUri(request, code); + await this.revokeAuthorizationCode(code); + + return this.saveToken( + code.user, + client, + code.authorizationCode, + code.scope, + ); + } + + /** + * Get the authorization code. + */ + + async getAuthorizationCode(request: Request, client: Client) { + if (!request.body.code) { + throw new InvalidRequestError('Missing parameter: `code`'); + } + + if (!is.vschar(request.body.code)) { + throw new InvalidRequestError('Invalid parameter: `code`'); + } + + const code = await this.model.getAuthorizationCode(request.body.code); + if (!code) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + if (!code.client) { + throw new ServerError( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + } + + if (!code.user) { + throw new ServerError( + 'Server error: `getAuthorizationCode()` did not return a `user` object', + ); + } + + if (code.client.id !== client.id) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + if (!(code.expiresAt instanceof Date)) { + throw new ServerError( + 'Server error: `expiresAt` must be a Date instance', + ); + } + + if (code.expiresAt.getTime() < Date.now()) { + throw new InvalidGrantError( + 'Invalid grant: authorization code has expired', + ); + } + + if (code.redirectUri && !is.uri(code.redirectUri)) { + throw new InvalidGrantError( + 'Invalid grant: `redirect_uri` is not a valid URI', + ); + } + + return code; + } + + /** + * Validate the redirect URI. + * + * "The authorization server MUST ensure that the redirect_uri parameter is + * present if the redirect_uri parameter was included in the initial + * authorization request as described in Section 4.1.1, and if included + * ensure that their values are identical." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + + validateRedirectUri(request: Request, code: AuthorizationCode) { + if (!code.redirectUri) { + return; + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (!is.uri(redirectUri)) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + + if (redirectUri !== code.redirectUri) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is invalid', + ); + } + } + + /** + * Revoke the authorization code. + * + * "The authorization code MUST expire shortly after it is issued to mitigate + * the risk of leaks. [...] If an authorization code is used more than once, + * the authorization server MUST deny the request." + * + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 + */ + + async revokeAuthorizationCode(code: AuthorizationCode) { + const status = await this.model.revokeAuthorizationCode(code); + if (!status) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + return code; + } + + /** + * Save token. + */ + + async saveToken( + user: User, + client: Client, + authorizationCode: string, + scope: string, + ) { + const accessScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = this.getRefreshTokenExpiresAt(); + + const token: Token = { + accessToken, + authorizationCode, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: accessScope, + } as any; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js deleted file mode 100644 index 138333e50..000000000 --- a/lib/grant-types/client-credentials-grant-type.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var util = require('util'); - -/** - * Constructor. - */ - -function ClientCredentialsGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getUserFromClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getUserFromClient()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(ClientCredentialsGrantType, AbstractGrantType); - -/** - * Handle client credentials grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 - */ - -ClientCredentialsGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - var scope = this.getScope(request); - - return Promise.bind(this) - .then(function() { - return this.getUserFromClient(client); - }) - .then(function(user) { - return this.saveToken(user, client, scope); - }); -}; - -/** - * Retrieve the user using client credentials. - */ - -ClientCredentialsGrantType.prototype.getUserFromClient = function(client) { - return promisify(this.model.getUserFromClient, 1).call(this.model, client) - .then(function(user) { - if (!user) { - throw new InvalidGrantError('Invalid grant: user credentials are invalid'); - } - - return user; - }); -}; - -/** - * Save token. - */ - -ClientCredentialsGrantType.prototype.saveToken = function(user, client, scope) { - var fns = [ - this.validateScope(user, client, scope), - this.generateAccessToken(client, user, scope), - this.getAccessTokenExpiresAt(client, user, scope) - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(scope, accessToken, accessTokenExpiresAt) { - var token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - scope: scope - }; - - return promisify(this.model.saveToken, 3).call(this.model, token, client, user); - }); -}; - -/** - * Export constructor. - */ - -module.exports = ClientCredentialsGrantType; diff --git a/lib/grant-types/client-credentials-grant-type.ts b/lib/grant-types/client-credentials-grant-type.ts new file mode 100755 index 000000000..80736bbac --- /dev/null +++ b/lib/grant-types/client-credentials-grant-type.ts @@ -0,0 +1,79 @@ +import { AbstractGrantType } from '.'; +import { InvalidArgumentError, InvalidGrantError } from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { Request } from '../request'; + +export class ClientCredentialsGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getUserFromClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getUserFromClient()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Handle client credentials grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const scope = this.getScope(request); + const user = await this.getUserFromClient(client); + + return this.saveToken(user, client, scope); + } + + /** + * Retrieve the user using client credentials. + */ + + async getUserFromClient(client: Client) { + const user = await this.model.getUserFromClient(client); + if (!user) { + throw new InvalidGrantError( + 'Invalid grant: user credentials are invalid', + ); + } + + return user; + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const accessScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + scope: accessScope, + } as Token; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/implicit-grant-type.js b/lib/grant-types/implicit-grant-type.js deleted file mode 100644 index f79963b3b..000000000 --- a/lib/grant-types/implicit-grant-type.js +++ /dev/null @@ -1,88 +0,0 @@ -'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/grant-types/implicit-grant-type.ts b/lib/grant-types/implicit-grant-type.ts new file mode 100644 index 000000000..a37670482 --- /dev/null +++ b/lib/grant-types/implicit-grant-type.ts @@ -0,0 +1,63 @@ +import { AbstractGrantType } from '.'; +import { InvalidArgumentError } from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { Request } from '../request'; + +export class ImplicitGrantType extends AbstractGrantType { + scope: string; + user: User; + constructor(options: any = {}) { + super(options); + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + + if (!options.user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } + + this.scope = options.scope; + this.user = options.user; + } + + /** + * Handle implicit token grant. + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + return this.saveToken(this.user, client, this.scope); + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const validatedScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + scope: validatedScope, + } as Token; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/index.ts b/lib/grant-types/index.ts new file mode 100644 index 000000000..3d0ad0d4b --- /dev/null +++ b/lib/grant-types/index.ts @@ -0,0 +1,6 @@ +export { AbstractGrantType } from './abstract-grant-type'; +export { AuthorizationCodeGrantType } from './authorization-code-grant-type'; +export { ClientCredentialsGrantType } from './client-credentials-grant-type'; +export { ImplicitGrantType } from './implicit-grant-type'; +export { PasswordGrantType } from './password-grant-type'; +export { RefreshTokenGrantType } from './refresh-token-grant-type'; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js deleted file mode 100644 index b7f17935b..000000000 --- a/lib/grant-types/password-grant-type.js +++ /dev/null @@ -1,133 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var is = require('../validator/is'); -var util = require('util'); - -/** - * Constructor. - */ - -function PasswordGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getUser) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getUser()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(PasswordGrantType, AbstractGrantType); - -/** - * Retrieve the user from the model using a username/password combination. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 - */ - -PasswordGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - var scope = this.getScope(request); - - return Promise.bind(this) - .then(function() { - return this.getUser(request); - }) - .then(function(user) { - return this.saveToken(user, client, scope); - }); -}; - -/** - * Get user using a username/password combination. - */ - -PasswordGrantType.prototype.getUser = function(request) { - if (!request.body.username) { - throw new InvalidRequestError('Missing parameter: `username`'); - } - - if (!request.body.password) { - throw new InvalidRequestError('Missing parameter: `password`'); - } - - if (!is.uchar(request.body.username)) { - throw new InvalidRequestError('Invalid parameter: `username`'); - } - - if (!is.uchar(request.body.password)) { - throw new InvalidRequestError('Invalid parameter: `password`'); - } - - return promisify(this.model.getUser, 2).call(this.model, request.body.username, request.body.password) - .then(function(user) { - if (!user) { - throw new InvalidGrantError('Invalid grant: user credentials are invalid'); - } - - return user; - }); -}; - -/** - * Save token. - */ - -PasswordGrantType.prototype.saveToken = function(user, client, scope) { - var fns = [ - this.validateScope(user, client, scope), - this.generateAccessToken(client, user, scope), - this.generateRefreshToken(client, user, scope), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(scope, accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { - var token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, - scope: scope - }; - - return promisify(this.model.saveToken, 3).call(this.model, token, client, user); - }); -}; - -/** - * Export constructor. - */ - -module.exports = PasswordGrantType; diff --git a/lib/grant-types/password-grant-type.ts b/lib/grant-types/password-grant-type.ts new file mode 100755 index 000000000..ca07b06ed --- /dev/null +++ b/lib/grant-types/password-grant-type.ts @@ -0,0 +1,108 @@ +import { AbstractGrantType } from '.'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, +} from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { Request } from '../request'; +import * as is from '../validator/is'; + +export class PasswordGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getUser) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getUser()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Retrieve the user from the model using a username/password combination. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 + */ + + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const scope = this.getScope(request); + const user = await this.getUser(request); + + return this.saveToken(user, client, scope); + } + + /** + * Get user using a username/password combination. + */ + + async getUser(request: Request) { + if (!request.body.username) { + throw new InvalidRequestError('Missing parameter: `username`'); + } + + if (!request.body.password) { + throw new InvalidRequestError('Missing parameter: `password`'); + } + + if (!is.uchar(request.body.username)) { + throw new InvalidRequestError('Invalid parameter: `username`'); + } + + if (!is.uchar(request.body.password)) { + throw new InvalidRequestError('Invalid parameter: `password`'); + } + + const user = await this.model.getUser( + request.body.username, + request.body.password, + ); + if (!user) { + throw new InvalidGrantError( + 'Invalid grant: user credentials are invalid', + ); + } + + return user; + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const accessScope = await this.validateScope(user, client, scope); + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = this.getRefreshTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: accessScope, + } as Token; + + return this.model.saveToken(token, client, user); + } +} diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js deleted file mode 100644 index 19f9010c2..000000000 --- a/lib/grant-types/refresh-token-grant-type.js +++ /dev/null @@ -1,180 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('./abstract-grant-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidGrantError = require('../errors/invalid-grant-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var ServerError = require('../errors/server-error'); -var is = require('../validator/is'); -var util = require('util'); - -/** - * Constructor. - */ - -function RefreshTokenGrantType(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getRefreshToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`'); - } - - if (!options.model.revokeToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - AbstractGrantType.call(this, options); -} - -/** - * Inherit prototype. - */ - -util.inherits(RefreshTokenGrantType, AbstractGrantType); - -/** - * Handle refresh token grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-6 - */ - -RefreshTokenGrantType.prototype.handle = function(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - return Promise.bind(this) - .then(function() { - return this.getRefreshToken(request, client); - }) - .tap(function(token) { - return this.revokeToken(token); - }) - .then(function(token) { - return this.saveToken(token.user, client, token.scope); - }); -}; - -/** - * Get refresh token. - */ - -RefreshTokenGrantType.prototype.getRefreshToken = function(request, client) { - if (!request.body.refresh_token) { - throw new InvalidRequestError('Missing parameter: `refresh_token`'); - } - - if (!is.vschar(request.body.refresh_token)) { - throw new InvalidRequestError('Invalid parameter: `refresh_token`'); - } - - return promisify(this.model.getRefreshToken, 1).call(this.model, request.body.refresh_token) - .then(function(token) { - if (!token) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - if (!token.client) { - throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object'); - } - - if (!token.user) { - throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object'); - } - - if (token.client.id !== client.id) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { - throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); - } - - if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { - throw new InvalidGrantError('Invalid grant: refresh token has expired'); - } - - return token; - }); -}; - -/** - * Revoke the refresh token. - * - * @see https://tools.ietf.org/html/rfc6749#section-6 - */ - -RefreshTokenGrantType.prototype.revokeToken = function(token) { - if (this.alwaysIssueNewRefreshToken === false) { - return Promise.resolve(token); - } - - return promisify(this.model.revokeToken, 1).call(this.model, token) - .then(function(status) { - if (!status) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - return token; - }); -}; - -/** - * Save token. - */ - -RefreshTokenGrantType.prototype.saveToken = function(user, client, scope) { - var fns = [ - this.generateAccessToken(client, user, scope), - this.generateRefreshToken(client, user, scope), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { - var token = { - accessToken: accessToken, - accessTokenExpiresAt: accessTokenExpiresAt, - scope: scope - }; - - if (this.alwaysIssueNewRefreshToken !== false) { - token.refreshToken = refreshToken; - token.refreshTokenExpiresAt = refreshTokenExpiresAt; - } - - return token; - }) - .then(function(token) { - return promisify(this.model.saveToken, 3).call(this.model, token, client, user) - .then(function(savedToken) { - return savedToken; - }); - }); -}; - -/** - * Export constructor. - */ - -module.exports = RefreshTokenGrantType; diff --git a/lib/grant-types/refresh-token-grant-type.ts b/lib/grant-types/refresh-token-grant-type.ts new file mode 100755 index 000000000..8e7d962e0 --- /dev/null +++ b/lib/grant-types/refresh-token-grant-type.ts @@ -0,0 +1,158 @@ +import { AbstractGrantType } from '.'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../errors'; +import { Client, RefreshToken, User } from '../interfaces'; +import { Request } from '../request'; +import * as is from '../validator/is'; + +export class RefreshTokenGrantType extends AbstractGrantType { + constructor(options: any = {}) { + super(options); + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + } + + /** + * Handle refresh token grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async handle(request: Request, client: Client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const token = await this.getRefreshToken(request, client); + await this.revokeToken(token); + + return this.saveToken(token.user, client, token.scope); + } + + /** + * Get refresh token. + */ + + async getRefreshToken(request: Request, client: Client) { + if (!request.body.refresh_token) { + throw new InvalidRequestError('Missing parameter: `refresh_token`'); + } + + if (!is.vschar(request.body.refresh_token)) { + throw new InvalidRequestError('Invalid parameter: `refresh_token`'); + } + + const token = await this.model.getRefreshToken(request.body.refresh_token); + + if (!token) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if (!token.client) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + } + + if (!token.user) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + } + + if (token.client.id !== client.id) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if ( + token.refreshTokenExpiresAt && + !(token.refreshTokenExpiresAt instanceof Date) + ) { + throw new ServerError( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + } + + if ( + token.refreshTokenExpiresAt && + token.refreshTokenExpiresAt.getTime() < Date.now() + ) { + throw new InvalidGrantError('Invalid grant: refresh token has expired'); + } + + return token; + } + + /** + * Revoke the refresh token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async revokeToken(token: RefreshToken) { + if (this.alwaysIssueNewRefreshToken === false) { + return token; + } + + const status = await this.model.revokeToken(token); + if (!status) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + return token; + } + + /** + * Save token. + */ + + async saveToken(user: User, client: Client, scope: string) { + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = this.getRefreshTokenExpiresAt(); + + const token: any = { + accessToken, + accessTokenExpiresAt, + scope, + }; + + if (this.alwaysIssueNewRefreshToken !== false) { + token.refreshToken = refreshToken; + token.refreshTokenExpiresAt = refreshTokenExpiresAt; + } + + const savedToken = await this.model.saveToken(token, client, user); + + return savedToken; + } +} diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js deleted file mode 100644 index 590c476a2..000000000 --- a/lib/handlers/authenticate-handler.js +++ /dev/null @@ -1,266 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var InsufficientScopeError = require('../errors/insufficient-scope-error'); -var InvalidTokenError = require('../errors/invalid-token-error'); -var OAuthError = require('../errors/oauth-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var Request = require('../request'); -var Response = require('../response'); -var ServerError = require('../errors/server-error'); -var UnauthorizedRequestError = require('../errors/unauthorized-request-error'); - -/** - * Constructor. - */ - -function AuthenticateHandler(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getAccessToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`'); - } - - if (options.scope && undefined === options.addAcceptedScopesHeader) { - throw new InvalidArgumentError('Missing parameter: `addAcceptedScopesHeader`'); - } - - if (options.scope && undefined === options.addAuthorizedScopesHeader) { - throw new InvalidArgumentError('Missing parameter: `addAuthorizedScopesHeader`'); - } - - if (options.scope && !options.model.verifyScope) { - throw new InvalidArgumentError('Invalid argument: model does not implement `verifyScope()`'); - } - - this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; - this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; - this.allowBearerTokensInQueryString = options.allowBearerTokensInQueryString; - this.model = options.model; - this.scope = options.scope; -} - -/** - * Authenticate Handler. - */ - -AuthenticateHandler.prototype.handle = function(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - // Extend model object with request - this.model.request = request; - - return Promise.bind(this) - .then(function() { - return this.getTokenFromRequest(request); - }) - .then(function(token) { - return this.getAccessToken(token); - }) - .tap(function(token) { - return this.validateAccessToken(token); - }) - .tap(function(token) { - if (!this.scope) { - return; - } - - return this.verifyScope(token); - }) - .tap(function(token) { - return this.updateResponse(response, token); - }) - .catch(function(e) { - // Include the "WWW-Authenticate" response header field if the client - // lacks any authentication information. - // - // @see https://tools.ietf.org/html/rfc6750#section-3.1 - if (e instanceof UnauthorizedRequestError) { - response.set('WWW-Authenticate', 'Bearer realm="Service"'); - } - - if (!(e instanceof OAuthError)) { - throw new ServerError(e); - } - - throw e; - }); -}; - -/** - * Get the token from the header or body, depending on the request. - * - * "Clients MUST NOT use more than one method to transmit the token in each request." - * - * @see https://tools.ietf.org/html/rfc6750#section-2 - */ - -AuthenticateHandler.prototype.getTokenFromRequest = function(request) { - var headerToken = request.get('Authorization'); - var queryToken = request.query.access_token; - var bodyToken = request.body.access_token; - - if (!!headerToken + !!queryToken + !!bodyToken > 1) { - throw new InvalidRequestError('Invalid request: only one authentication method is allowed'); - } - - if (headerToken) { - return this.getTokenFromRequestHeader(request); - } - - if (queryToken) { - return this.getTokenFromRequestQuery(request); - } - - if (bodyToken) { - return this.getTokenFromRequestBody(request); - } - - throw new UnauthorizedRequestError('Unauthorized request: no authentication given'); -}; - -/** - * Get the token from the request header. - * - * @see http://tools.ietf.org/html/rfc6750#section-2.1 - */ - -AuthenticateHandler.prototype.getTokenFromRequestHeader = function(request) { - var token = request.get('Authorization'); - var matches = token.match(/Bearer\s(\S+)/); - - if (!matches) { - throw new InvalidRequestError('Invalid request: malformed authorization header'); - } - - return matches[1]; -}; - -/** - * Get the token from the request query. - * - * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page - * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be - * passed in HTTP message headers or message bodies for which confidentiality measures - * are taken. Browsers, web servers, and other software may not adequately secure URLs - * in the browser history, web server logs, and other data structures. If bearer tokens - * are passed in page URLs, attackers might be able to steal them from the history data, - * logs, or other unsecured locations." - * - * @see http://tools.ietf.org/html/rfc6750#section-2.3 - */ - -AuthenticateHandler.prototype.getTokenFromRequestQuery = function(request) { - if (!this.allowBearerTokensInQueryString) { - throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); - } - - return request.query.access_token; -}; - -/** - * Get the token from the request body. - * - * "The HTTP request method is one for which the request-body has defined semantics. - * In particular, this means that the "GET" method MUST NOT be used." - * - * @see http://tools.ietf.org/html/rfc6750#section-2.2 - */ - -AuthenticateHandler.prototype.getTokenFromRequestBody = function(request) { - if (request.method === 'GET') { - throw new InvalidRequestError('Invalid request: token may not be passed in the body when using the GET verb'); - } - - if (!request.is('application/x-www-form-urlencoded')) { - throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'); - } - - return request.body.access_token; -}; - -/** - * Get the access token from the model. - */ - -AuthenticateHandler.prototype.getAccessToken = function(token) { - return promisify(this.model.getAccessToken, 1).call(this.model, token) - .then(function(accessToken) { - if (!accessToken) { - throw new InvalidTokenError('Invalid token: access token is invalid'); - } - - if (!accessToken.user) { - throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); - } - - return accessToken; - }); -}; - -/** - * Validate access token. - */ - -AuthenticateHandler.prototype.validateAccessToken = function(accessToken) { - if (!(accessToken.accessTokenExpiresAt instanceof Date)) { - throw new ServerError('Server error: `accessTokenExpiresAt` must be a Date instance'); - } - - if (accessToken.accessTokenExpiresAt < new Date()) { - throw new InvalidTokenError('Invalid token: access token has expired'); - } - - return accessToken; -}; - -/** - * Verify scope. - */ - -AuthenticateHandler.prototype.verifyScope = function(accessToken) { - return promisify(this.model.verifyScope, 2).call(this.model, accessToken, this.scope) - .then(function(scope) { - if (!scope) { - throw new InsufficientScopeError('Insufficient scope: authorized scope is insufficient'); - } - - return scope; - }); -}; - -/** - * Update response. - */ - -AuthenticateHandler.prototype.updateResponse = function(response, accessToken) { - if (this.scope && this.addAcceptedScopesHeader) { - response.set('X-Accepted-OAuth-Scopes', this.scope); - } - - if (this.scope && this.addAuthorizedScopesHeader) { - response.set('X-OAuth-Scopes', accessToken.scope); - } -}; - -/** - * Export constructor. - */ - -module.exports = AuthenticateHandler; diff --git a/lib/handlers/authenticate-handler.ts b/lib/handlers/authenticate-handler.ts new file mode 100755 index 000000000..67b30b0a1 --- /dev/null +++ b/lib/handlers/authenticate-handler.ts @@ -0,0 +1,273 @@ +import { + InsufficientScopeError, + InvalidArgumentError, + InvalidRequestError, + InvalidTokenError, + OAuthError, + ServerError, + UnauthorizedRequestError, +} from '../errors'; +import { Model, Token } from '../interfaces'; +import { Request } from '../request'; +import { Response } from '../response'; + +export class AuthenticateHandler { + addAcceptedScopesHeader: any; + addAuthorizedScopesHeader: any; + allowBearerTokensInQueryString: any; + model: Model; + scope: any; + constructor(options: any = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAccessToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + + if (options.scope && options.addAcceptedScopesHeader === undefined) { + throw new InvalidArgumentError( + 'Missing parameter: `addAcceptedScopesHeader`', + ); + } + + if (options.scope && options.addAuthorizedScopesHeader === undefined) { + throw new InvalidArgumentError( + 'Missing parameter: `addAuthorizedScopesHeader`', + ); + } + + if (options.scope && !options.model.verifyScope) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `verifyScope()`', + ); + } + + this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; + this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; + this.allowBearerTokensInQueryString = + options.allowBearerTokensInQueryString; + this.model = options.model; + this.scope = options.scope; + } + + /** + * Authenticate Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + // Extend model object with request + this.model.request = request; + + try { + let token = await this.getTokenFromRequest(request); + token = await this.getAccessToken(token); + this.validateAccessToken(token); + if (this.scope) { + await this.verifyScope(token); + } + this.updateResponse(response, token); + + return token; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // lacks any authentication information. + // + // @see https://tools.ietf.org/html/rfc6750#section-3.1 + if (e instanceof UnauthorizedRequestError) { + response.set('WWW-Authenticate', 'Bearer realm="Service"'); + } + + if (!(e instanceof OAuthError)) { + throw new ServerError(e); + } + + throw e; + } + } + + /** + * Get the token from the header or body, depending on the request. + * + * "Clients MUST NOT use more than one method to transmit the token in each request." + * + * @see https://tools.ietf.org/html/rfc6750#section-2 + */ + + getTokenFromRequest(request: Request) { + const headerToken = request.get('Authorization'); + const queryToken = request.query.access_token; + const bodyToken = request.body.access_token; + + if ([headerToken, queryToken, bodyToken].filter(Boolean).length > 1) { + throw new InvalidRequestError( + 'Invalid request: only one authentication method is allowed', + ); + } + + if (headerToken) { + return this.getTokenFromRequestHeader(request); + } + + if (queryToken) { + return this.getTokenFromRequestQuery(request); + } + + if (bodyToken) { + return this.getTokenFromRequestBody(request); + } + + throw new UnauthorizedRequestError( + 'Unauthorized request: no authentication given', + ); + } + + /** + * Get the token from the request header. + * + * @see http://tools.ietf.org/html/rfc6750#section-2.1 + */ + + getTokenFromRequestHeader(request: Request) { + const token = request.get('Authorization'); + const matches = token.match(/Bearer\s(\S+)/); + + if (!matches) { + throw new InvalidRequestError( + 'Invalid request: malformed authorization header', + ); + } + + return matches[1]; + } + + /** + * Get the token from the request query. + * + * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page + * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be + * passed in HTTP message headers or message bodies for which confidentiality measures + * are taken. Browsers, web servers, and other software may not adequately secure URLs + * in the browser history, web server logs, and other data structures. If bearer tokens + * are passed in page URLs, attackers might be able to steal them from the history data, + * logs, or other unsecured locations." + * + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + */ + + getTokenFromRequestQuery(request: Request) { + if (!this.allowBearerTokensInQueryString) { + throw new InvalidRequestError( + 'Invalid request: do not send bearer tokens in query URLs', + ); + } + + return request.query.access_token; + } + + /** + * Get the token from the request body. + * + * "The HTTP request method is one for which the request-body has defined semantics. + * In particular, this means that the "GET" method MUST NOT be used." + * + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + */ + + getTokenFromRequestBody(request: Request) { + if (request.method === 'GET') { + throw new InvalidRequestError( + 'Invalid request: token may not be passed in the body when using the GET verb', + ); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + + return request.body.access_token; + } + + /** + * Get the access token from the model. + */ + + async getAccessToken(token: string) { + const accessToken = await this.model.getAccessToken(token); + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (!accessToken.user) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + } + + return accessToken; + } + + /** + * Validate access token. + */ + + validateAccessToken(accessToken: Token) { + if (!(accessToken.accessTokenExpiresAt instanceof Date)) { + throw new ServerError( + 'Server error: `accessTokenExpiresAt` must be a Date instance', + ); + } + + if (accessToken.accessTokenExpiresAt.getTime() < Date.now()) { + throw new InvalidTokenError('Invalid token: access token has expired'); + } + + return accessToken; + } + + /** + * Verify scope. + */ + + async verifyScope(accessToken: Token) { + const scope = await this.model.verifyScope(accessToken, this.scope); + if (!scope) { + throw new InsufficientScopeError( + 'Insufficient scope: authorized scope is insufficient', + ); + } + + return scope; + } + + /** + * Update response. + */ + + updateResponse(response: Response, accessToken: Token) { + if (this.scope && this.addAcceptedScopesHeader) { + response.set('X-Accepted-OAuth-Scopes', this.scope); + } + + if (this.scope && this.addAuthorizedScopesHeader) { + response.set('X-OAuth-Scopes', accessToken.scope); + } + } +} diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js deleted file mode 100644 index 3c14d2290..000000000 --- a/lib/handlers/authorize-handler.js +++ /dev/null @@ -1,332 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var AccessDeniedError = require('../errors/access-denied-error'); -var AuthenticateHandler = require('../handlers/authenticate-handler'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidClientError = require('../errors/invalid-client-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var InvalidScopeError = require('../errors/invalid-scope-error'); -var UnsupportedResponseTypeError = require('../errors/unsupported-response-type-error'); -var OAuthError = require('../errors/oauth-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var Request = require('../request'); -var Response = require('../response'); -var ServerError = require('../errors/server-error'); -var UnauthorizedClientError = require('../errors/unauthorized-client-error'); -var is = require('../validator/is'); -var url = require('url'); - -/** - * Response types. - */ - -var responseTypes = { - code: require('../response-types/code-response-type'), - token: require('../response-types/token-response-type') -}; - -/** - * Constructor. - */ - -function AuthorizeHandler(options) { - options = options || {}; - - if (options.authenticateHandler && !options.authenticateHandler.handle) { - throw new InvalidArgumentError('Invalid argument: authenticateHandler does not implement `handle()`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); - } - - this.options = options; - this.allowEmptyState = options.allowEmptyState; - this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); - this.model = options.model; -} - -/** - * Authorize Handler. - */ - -AuthorizeHandler.prototype.handle = function(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - if ('false' === request.query.allowed) { - return Promise.reject(new AccessDeniedError('Access denied: user denied access to application')); - } - - // Extend model object with request - this.model.request = request; - - var fns = [ - this.getClient(request), - this.getUser(request, response) - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(client, user) { - var scope; - var state; - var RequestedResponseType; - var responseType; - var uri = this.getRedirectUri(request, client); - - return Promise - .bind(this) - .then(function() { - var requestedScope = this.getScope(request); - - return this.validateScope(user, client, requestedScope); - }) - .then(function(validScope) { - scope = validScope; - state = this.getState(request); - - RequestedResponseType = this.getResponseType(request, client); - responseType = new RequestedResponseType(this.options); - - return responseType.handle(request, client, user, uri, scope); - }) - .then(function(codeOrAccessToken) { - var redirectUri = this.buildSuccessRedirectUri(uri, responseType); - - this.updateResponse(response, redirectUri, responseType, state); - - return codeOrAccessToken; - }) - .catch(function(e) { - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - - var redirectUri = this.buildErrorRedirectUri(uri, responseType, e); - - this.updateResponse(response, redirectUri, responseType, state); - - throw e; - }); - }); -}; - -/** - * Get the client from the model. - */ - -AuthorizeHandler.prototype.getClient = function(request) { - var clientId = request.body.client_id || request.query.client_id; - - if (!clientId) { - throw new InvalidRequestError('Missing parameter: `client_id`'); - } - - if (!is.vschar(clientId)) { - throw new InvalidRequestError('Invalid parameter: `client_id`'); - } - - var redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (redirectUri && !is.uri(redirectUri)) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); - } - - return promisify(this.model.getClient, 2).call(this.model, clientId, null) - .then(function(client) { - if (!client) { - throw new InvalidClientError('Invalid client: client credentials are invalid'); - } - - if (!client.grants) { - throw new InvalidClientError('Invalid client: missing client `grants`'); - } - - 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'); - } - - if (!client.redirectUris || 0 === client.redirectUris.length) { - throw new InvalidClientError('Invalid client: missing client `redirectUri`'); - } - - if (redirectUri && !_.includes(client.redirectUris, redirectUri)) { - throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); - } - - return client; - }); -}; - -/** - * 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. - */ - -AuthorizeHandler.prototype.getScope = function(request) { - var scope = request.body.scope || request.query.scope; - - if (!is.nqschar(scope)) { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } - - return scope; -}; - -/** - * Get state from the request. - */ - -AuthorizeHandler.prototype.getState = function(request) { - var state = request.body.state || request.query.state; - - if (!this.allowEmptyState && !state) { - throw new InvalidRequestError('Missing parameter: `state`'); - } - - if (!is.vschar(state)) { - throw new InvalidRequestError('Invalid parameter: `state`'); - } - - return state; -}; - -/** - * Get user by calling the authenticate middleware. - */ - -AuthorizeHandler.prototype.getUser = function(request, response) { - if (this.authenticateHandler instanceof AuthenticateHandler) { - return this.authenticateHandler.handle(request, response).get('user'); - } - return promisify(this.authenticateHandler.handle, 2)(request, response).then(function(user) { - if (!user) { - throw new ServerError('Server error: `handle()` did not return a `user` object'); - } - - return user; - }); -}; - -/** - * Get redirect URI. - */ - -AuthorizeHandler.prototype.getRedirectUri = function(request, client) { - return request.body.redirect_uri || request.query.redirect_uri || client.redirectUris[0]; -}; - - -/** - * Get response type. - */ - -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`'); - } - - if (!_.has(responseTypes, responseType)) { - 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]; -}; - -/** - * Build a successful response that redirects the user-agent to the client-provided url. - */ - -AuthorizeHandler.prototype.buildSuccessRedirectUri = function(redirectUri, responseType) { - 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, responseType, error) { - var uri = url.parse(redirectUri); - - if (responseType) { - uri = responseType.setRedirectUriParam(uri, 'error', error.name); - - if (error.message) { - uri = responseType.setRedirectUriParam(uri, 'error_description', error.message); - } - } else { - uri.query = { - error: error.name - }; - - if (error.message) { - uri.query.error_description = error.message; - } - } - - return uri; -}; - -/** - * Update response with the redirect uri and the state parameter, if available. - */ - -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; - } - - response.redirect(url.format(redirectUri)); -}; - -/** - * Export constructor. - */ - -module.exports = AuthorizeHandler; diff --git a/lib/handlers/authorize-handler.ts b/lib/handlers/authorize-handler.ts new file mode 100755 index 000000000..93d67a6e9 --- /dev/null +++ b/lib/handlers/authorize-handler.ts @@ -0,0 +1,377 @@ +import * as url from 'url'; +import { AuthenticateHandler } from '.'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidScopeError, + OAuthError, + ServerError, + UnauthorizedClientError, + UnsupportedResponseTypeError, +} from '../errors'; +import { Client, Model, User } from '../interfaces'; +import { Request } from '../request'; +import { Response } from '../response'; +import { CodeResponseType, TokenResponseType } from '../response-types'; +import { hasOwnProperty } from '../utils/fn'; +import * as is from '../validator/is'; + +/** + * Response types. + */ + +const responseTypes = { + code: CodeResponseType, + token: TokenResponseType, +}; + +/** + * Constructor. + */ + +export class AuthorizeHandler { + options: any; + allowEmptyState: boolean; + authenticateHandler: any; + model: Model; + constructor(options: any = {}) { + if (options.authenticateHandler && !options.authenticateHandler.handle) { + throw new InvalidArgumentError( + 'Invalid argument: authenticateHandler does not implement `handle()`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + this.options = options; + this.allowEmptyState = options.allowEmptyState; + this.authenticateHandler = + options.authenticateHandler || new AuthenticateHandler(options); + this.model = options.model; + } + + /** + * Authorize Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.query.allowed === 'false') { + throw new AccessDeniedError( + 'Access denied: user denied access to application', + ); + } + + // Extend model object with request + this.model.request = request; + + const client = await this.getClient(request); + const user = await this.getUser(request, response); + + let scope: string; + let state: string; + let RequestedResponseType: any; + let responseType: any; + const uri = this.getRedirectUri(request, client); + try { + const requestedScope = this.getScope(request); + + const validScope = await this.validateScope(user, client, requestedScope); + scope = validScope; + state = this.getState(request); + RequestedResponseType = this.getResponseType(request, client); + responseType = new RequestedResponseType(this.options); + const codeOrAccessToken = await responseType.handle( + request, + client, + user, + uri, + scope, + ); + const redirectUri = this.buildSuccessRedirectUri(uri, responseType); + this.updateResponse(response, redirectUri, responseType, state); + + return codeOrAccessToken; + } catch (e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + + const redirectUri = this.buildErrorRedirectUri(uri, responseType, e); + + this.updateResponse(response, redirectUri, responseType, state); + + throw e; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request: Request) { + const clientId = request.body.client_id || request.query.client_id; + + if (!clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if (!is.vschar(clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (redirectUri && !is.uri(redirectUri)) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + + const client = await this.model.getClient(clientId); + if (!client) { + throw new InvalidClientError( + 'Invalid client: client credentials are invalid', + ); + } + + if (!client.grants) { + throw new InvalidClientError('Invalid client: missing client `grants`'); + } + + const responseType = + request.body.response_type || request.query.response_type; + const requestedGrantType = + responseType === 'token' ? 'implicit' : 'authorization_code'; + + if (!client.grants.includes(requestedGrantType)) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + if (!client.redirectUris || client.redirectUris.length === 0) { + throw new InvalidClientError( + 'Invalid client: missing client `redirectUri`', + ); + } + + if (redirectUri && !client.redirectUris.includes(redirectUri)) { + throw new InvalidClientError( + 'Invalid client: `redirect_uri` does not match client value', + ); + } + + return client; + } + + /** + * Validate requested scope. + */ + async validateScope(user: User, client: Client, scope: string) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + if (!validatedScope) { + throw new InvalidScopeError( + 'Invalid scope: Requested scope is invalid', + ); + } + + return validatedScope; + } + + return scope; + } + + /** + * Get scope from the request. + */ + + getScope(request: Request) { + const scope = request.body.scope || request.query.scope; + + if (!is.nqschar(scope)) { + throw new InvalidScopeError('Invalid parameter: `scope`'); + } + + return scope; + } + + /** + * Get state from the request. + */ + + getState(request: Request) { + const state = request.body.state || request.query.state; + + if (!this.allowEmptyState && !state) { + throw new InvalidRequestError('Missing parameter: `state`'); + } + + if (!is.vschar(state)) { + throw new InvalidRequestError('Invalid parameter: `state`'); + } + + return state; + } + + /** + * Get user by calling the authenticate middleware. + */ + + async getUser(request: Request, response: Response) { + if (this.authenticateHandler instanceof AuthenticateHandler) { + const data = await this.authenticateHandler.handle(request, response); + + return data.user; + } + + const user = await this.authenticateHandler.handle(request, response); + if (!user) { + throw new ServerError( + 'Server error: `handle()` did not return a `user` object', + ); + } + + return user; + } + + /** + * Get redirect URI. + */ + + getRedirectUri(request: Request, client: Client) { + return ( + request.body.redirect_uri || + request.query.redirect_uri || + client.redirectUris[0] + ); + } + + /** + * Get response type. + */ + + getResponseType(request: Request, client: Client) { + const responseType = + request.body.response_type || request.query.response_type; + + if (!responseType) { + throw new InvalidRequestError('Missing parameter: `response_type`'); + } + + if (!hasOwnProperty(responseTypes, responseType)) { + throw new UnsupportedResponseTypeError( + 'Unsupported response type: `response_type` is not supported', + ); + } + + if ( + responseType === 'token' && + (!client || !client.grants.includes('implicit')) + ) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + return responseTypes[responseType]; + } + + /** + * Build a successful response that redirects the user-agent to the client-provided url. + */ + + buildSuccessRedirectUri( + redirectUri: string, + responseType: CodeResponseType | TokenResponseType, + ) { + const uri = url.parse(redirectUri); + + return responseType.buildRedirectUri(uri); + } + + /** + * Build an error response that redirects the user-agent to the client-provided url. + */ + + buildErrorRedirectUri( + redirectUri: any, + responseType: CodeResponseType | TokenResponseType, + error: Error, + ) { + let uri = url.parse(redirectUri, true); + + if (responseType) { + uri = responseType.setRedirectUriParam(uri, 'error', error.name); + + if (error.message) { + uri = responseType.setRedirectUriParam( + uri, + 'error_description', + error.message, + ); + } + } else { + uri.query = { + error: error.name, + }; + + if (error.message) { + uri.query.error_description = error.message; + } + } + + return uri; + } + + /** + * Update response with the redirect uri and the state parameter, if available. + */ + + updateResponse( + response: Response, + redirectUri: any, + responseType: CodeResponseType | TokenResponseType, + state: any, + ) { + if (responseType && state) { + // tslint:disable-next-line:no-parameter-reassignment + redirectUri = responseType.setRedirectUriParam( + redirectUri, + 'state', + state, + ); + } else if (state) { + redirectUri.query = redirectUri.query || {}; + redirectUri.query.state = state; + } + + response.redirect(url.format(redirectUri)); + } +} diff --git a/lib/handlers/index.ts b/lib/handlers/index.ts new file mode 100644 index 000000000..7bdf800ee --- /dev/null +++ b/lib/handlers/index.ts @@ -0,0 +1,4 @@ +export { AuthenticateHandler } from './authenticate-handler'; +export { AuthorizeHandler } from './authorize-handler'; +export { RevokeHandler } from './revoke-handler'; +export { TokenHandler } from './token-handler'; diff --git a/lib/handlers/revoke-handler.js b/lib/handlers/revoke-handler.js deleted file mode 100644 index 0624b747d..000000000 --- a/lib/handlers/revoke-handler.js +++ /dev/null @@ -1,326 +0,0 @@ -'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/revoke-handler.ts b/lib/handlers/revoke-handler.ts new file mode 100644 index 000000000..0636f99fa --- /dev/null +++ b/lib/handlers/revoke-handler.ts @@ -0,0 +1,341 @@ +import * as auth from 'basic-auth'; +import { + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidTokenError, + OAuthError, + ServerError, +} from '../errors'; +import { Client, Model } from '../interfaces'; +import { Request } from '../request'; +import { Response } from '../response'; +import { oneSuccess } from '../utils/fn'; +import * as is from '../validator/is'; + +export class RevokeHandler { + model: Model; + constructor(options: any = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + + if (!options.model.getAccessToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + + this.model = options.model; + } + + /** + * Revoke Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.method !== 'POST') { + throw new InvalidRequestError('Invalid request: method must be POST'); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + // Extend model object with request + this.model.request = request; + + try { + const client = await this.getClient(request, response); + + return this.handleRevokeToken(request, client); + } catch (e) { + let error = e; + if (!(error instanceof OAuthError)) { + error = new ServerError(error); + } + /** + * All necessary information is conveyed in the response code. + * + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * @see https://tools.ietf.org/html/rfc7009#section-2.2 + */ + if (!(error instanceof InvalidTokenError)) { + this.updateErrorResponse(response, error); + } + + throw error; + } + } + + /** + * Revoke a refresh or access token. + * + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" + */ + + async handleRevokeToken(request: Request, client: Client) { + try { + let token = await this.getTokenFromRequest(request); + token = await oneSuccess([ + this.getAccessToken(token, client), + this.getRefreshToken(token, client), + ]); + + return this.revokeToken(token); + } catch (errors) { + throw errors; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request: Request, response: Response) { + const credentials = this.getClientCredentials(request); + + if (!credentials.clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if (!credentials.clientSecret) { + throw new InvalidRequestError('Missing parameter: `client_secret`'); + } + + if (!is.vschar(credentials.clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + if (!is.vschar(credentials.clientSecret)) { + throw new InvalidRequestError('Invalid parameter: `client_secret`'); + } + try { + const client = await this.model.getClient( + credentials.clientId, + credentials.clientSecret, + ); + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if (!client.grants) { + throw new ServerError('Server error: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError('Server error: `grants` must be an array'); + } + + return client; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if (e instanceof InvalidClientError && request.get('authorization')) { + response.set('WWW-Authenticate', 'Basic realm="Service"'); + + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + } + } + + /** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + getClientCredentials(request: Request) { + const credentials = auth(request as any); + + if (credentials) { + return { clientId: credentials.name, clientSecret: credentials.pass }; + } + + if (request.body.client_id && request.body.client_secret) { + return { + clientId: request.body.client_id, + clientSecret: request.body.client_secret, + }; + } + + throw new InvalidClientError( + 'Invalid client: cannot retrieve client credentials', + ); + } + + /** + * Get the token from the body. + * + * @see https://tools.ietf.org/html/rfc7009#section-2.1 + */ + + getTokenFromRequest(request: Request) { + const bodyToken = request.body.token; + + if (!bodyToken) { + throw new InvalidRequestError('Missing parameter: `token`'); + } + + return bodyToken; + } + + /** + * Get refresh token. + */ + + async getRefreshToken(token, client: Client) { + const refreshToken = await this.model.getRefreshToken(token); + if (!refreshToken) { + throw new InvalidTokenError('Invalid token: refresh token is invalid'); + } + + if (!refreshToken.client) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + } + + if (!refreshToken.user) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + } + + if (refreshToken.client.id !== client.id) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if ( + refreshToken.refreshTokenExpiresAt && + !(refreshToken.refreshTokenExpiresAt instanceof Date) + ) { + throw new ServerError( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + } + + if ( + refreshToken.refreshTokenExpiresAt && + refreshToken.refreshTokenExpiresAt.getTime() < Date.now() + ) { + throw new InvalidTokenError('Invalid token: refresh token has expired'); + } + + return refreshToken; + } + + /** + * Get the access token from the model. + */ + + async getAccessToken(token: string, client: Client) { + const accessToken = await this.model.getAccessToken(token); + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (!accessToken.client) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `client` object', + ); + } + + if (!accessToken.user) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + } + + if (accessToken.client.id !== client.id) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if ( + accessToken.accessTokenExpiresAt && + !(accessToken.accessTokenExpiresAt instanceof Date) + ) { + throw new ServerError('Server error: `expires` must be a Date instance'); + } + + if ( + accessToken.accessTokenExpiresAt && + accessToken.accessTokenExpiresAt.getTime() < Date.now() + ) { + throw new InvalidTokenError('Invalid token: access token has expired.'); + } + + return accessToken; + } + + /** + * Revoke the token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async revokeToken(token: any) { + const revokedToken = await this.model.revokeToken(token); + if (!revokedToken) { + throw new InvalidTokenError('Invalid token: token is invalid'); + } + + return revokedToken; + } + + /** + * Update response when an error is thrown. + */ + + updateErrorResponse(response: Response, error: OAuthError) { + response.body = { + error: error.name, + error_description: error.message, + }; + + response.status = error.code; + } +} diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js deleted file mode 100644 index af162ca38..000000000 --- a/lib/handlers/token-handler.js +++ /dev/null @@ -1,300 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var BearerTokenType = require('../token-types/bearer-token-type'); -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var InvalidClientError = require('../errors/invalid-client-error'); -var InvalidRequestError = require('../errors/invalid-request-error'); -var OAuthError = require('../errors/oauth-error'); -var Promise = require('bluebird'); -var promisify = require('promisify-any').use(Promise); -var Request = require('../request'); -var Response = require('../response'); -var ServerError = require('../errors/server-error'); -var TokenModel = require('../models/token-model'); -var UnauthorizedClientError = require('../errors/unauthorized-client-error'); -var UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error'); -var auth = require('basic-auth'); -var is = require('../validator/is'); - -/** - * Grant types. - */ - -var grantTypes = { - authorization_code: require('../grant-types/authorization-code-grant-type'), - client_credentials: require('../grant-types/client-credentials-grant-type'), - password: require('../grant-types/password-grant-type'), - refresh_token: require('../grant-types/refresh-token-grant-type') -}; - -/** - * Constructor. - */ - -function TokenHandler(options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.refreshTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `refreshTokenLifetime`'); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.grantTypes = _.assign({}, grantTypes, options.extendedGrantTypes); - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; - this.requireClientAuthentication = options.requireClientAuthentication || {}; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false; -} - -/** - * Token Handler. - */ - -TokenHandler.prototype.handle = function(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - if (request.method !== 'POST') { - return Promise.reject(new InvalidRequestError('Invalid request: method must be POST')); - } - - if (!request.is('application/x-www-form-urlencoded')) { - return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')); - } - - // Extend model object with request - this.model.request = request; - - return Promise.bind(this) - .then(function() { - return this.getClient(request, response); - }) - .then(function(client) { - return this.handleGrantType(request, client); - }) - .tap(function(data) { - var model = new TokenModel(data, {allowExtendedTokenAttributes: this.allowExtendedTokenAttributes}); - var tokenType = this.getTokenType(model); - - this.updateSuccessResponse(response, tokenType); - }).catch(function(e) { - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - - this.updateErrorResponse(response, e); - - throw e; - }); -}; - -/** - * Get the client from the model. - */ - -TokenHandler.prototype.getClient = function(request, response) { - var credentials = this.getClientCredentials(request); - var grantType = request.body.grant_type; - - if (!credentials.clientId) { - throw new InvalidRequestError('Missing parameter: `client_id`'); - } - - if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret) { - throw new InvalidRequestError('Missing parameter: `client_secret`'); - } - - if (!is.vschar(credentials.clientId)) { - throw new InvalidRequestError('Invalid parameter: `client_id`'); - } - - if (credentials.clientSecret && !is.vschar(credentials.clientSecret)) { - throw new InvalidRequestError('Invalid parameter: `client_secret`'); - } - - return promisify(this.model.getClient, 2).call(this.model, credentials.clientId, credentials.clientSecret) - .then(function(client) { - if (!client) { - throw new InvalidClientError('Invalid client: client is invalid'); - } - - if (!client.grants) { - throw new ServerError('Server error: missing client `grants`'); - } - - if (!(client.grants instanceof Array)) { - throw new ServerError('Server error: `grants` must be an array'); - } - - return client; - }) - .catch(function(e) { - // Include the "WWW-Authenticate" response header field if the client - // attempted to authenticate via the "Authorization" request header. - // - // @see https://tools.ietf.org/html/rfc6749#section-5.2. - if ((e instanceof InvalidClientError) && request.get('authorization')) { - response.set('WWW-Authenticate', 'Basic realm="Service"'); - - throw new InvalidClientError(e, { code: 401 }); - } - - throw e; - }); -}; - -/** - * Get client credentials. - * - * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, - * the `client_id` and `client_secret` can be embedded in the body. - * - * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 - */ - -TokenHandler.prototype.getClientCredentials = function(request) { - var credentials = auth(request); - var grantType = request.body.grant_type; - - if (credentials) { - return { clientId: credentials.name, clientSecret: credentials.pass }; - } - - if (request.body.client_id && request.body.client_secret) { - return { clientId: request.body.client_id, clientSecret: request.body.client_secret }; - } - - if (!this.isClientAuthenticationRequired(grantType)) { - if(request.body.client_id) { - return { clientId: request.body.client_id }; - } - } - - throw new InvalidClientError('Invalid client: cannot retrieve client credentials'); -}; - -/** - * Handle grant type. - */ - -TokenHandler.prototype.handleGrantType = function(request, client) { - var grantType = request.body.grant_type; - - if (!grantType) { - throw new InvalidRequestError('Missing parameter: `grant_type`'); - } - - if (!is.nchar(grantType) && !is.uri(grantType)) { - throw new InvalidRequestError('Invalid parameter: `grant_type`'); - } - - if (!_.has(this.grantTypes, grantType)) { - throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid'); - } - - if (!_.includes(client.grants, grantType)) { - throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); - } - - var accessTokenLifetime = this.getAccessTokenLifetime(client); - var refreshTokenLifetime = this.getRefreshTokenLifetime(client); - var Type = this.grantTypes[grantType]; - - var options = { - accessTokenLifetime: accessTokenLifetime, - model: this.model, - refreshTokenLifetime: refreshTokenLifetime, - alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken - }; - - return new Type(options) - .handle(request, client); -}; - -/** - * Get access token lifetime. - */ - -TokenHandler.prototype.getAccessTokenLifetime = function(client) { - return client.accessTokenLifetime || this.accessTokenLifetime; -}; - -/** - * Get refresh token lifetime. - */ - -TokenHandler.prototype.getRefreshTokenLifetime = function(client) { - return client.refreshTokenLifetime || this.refreshTokenLifetime; -}; - -/** - * Get token type. - */ - -TokenHandler.prototype.getTokenType = function(model) { - return new BearerTokenType(model.accessToken, model.accessTokenLifetime, model.refreshToken, model.scope, model.customAttributes); -}; - -/** - * Update response when a token is generated. - */ - -TokenHandler.prototype.updateSuccessResponse = function(response, tokenType) { - response.body = tokenType.valueOf(); - - response.set('Cache-Control', 'no-store'); - response.set('Pragma', 'no-cache'); -}; - -/** - * Update response when an error is thrown. - */ - -TokenHandler.prototype.updateErrorResponse = function(response, error) { - response.body = { - error: error.name, - error_description: error.message - }; - - response.status = error.code; -}; - -/** - * Given a grant type, check if client authentication is required - */ -TokenHandler.prototype.isClientAuthenticationRequired = function(grantType) { - if (Object.keys(this.requireClientAuthentication).length > 0) { - return (typeof this.requireClientAuthentication[grantType] !== 'undefined') ? this.requireClientAuthentication[grantType] : true; - } else { - return true; - } -}; - -/** - * Export constructor. - */ - -module.exports = TokenHandler; diff --git a/lib/handlers/token-handler.ts b/lib/handlers/token-handler.ts new file mode 100755 index 000000000..66d9ac363 --- /dev/null +++ b/lib/handlers/token-handler.ts @@ -0,0 +1,331 @@ +import * as auth from 'basic-auth'; +import { + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + OAuthError, + ServerError, + UnauthorizedClientError, + UnsupportedGrantTypeError, +} from '../errors'; +import { + AuthorizationCodeGrantType, + ClientCredentialsGrantType, + PasswordGrantType, + RefreshTokenGrantType, +} from '../grant-types'; +import { Client, Model } from '../interfaces'; +import { TokenModel } from '../models'; +import { Request } from '../request'; +import { Response } from '../response'; +import { BearerTokenType } from '../token-types'; +import { hasOwnProperty } from '../utils/fn'; +import * as is from '../validator/is'; + +/** + * Grant types. + */ + +const grantTypes = { + authorization_code: AuthorizationCodeGrantType, + client_credentials: ClientCredentialsGrantType, + password: PasswordGrantType, + refresh_token: RefreshTokenGrantType, +}; +export class TokenHandler { + accessTokenLifetime: any; + grantTypes: { [key: string]: any }; + model: Model; + refreshTokenLifetime: number; + allowExtendedTokenAttributes: boolean; + requireClientAuthentication: any; + alwaysIssueNewRefreshToken: boolean; + constructor(options: any = {}) { + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.refreshTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `refreshTokenLifetime`', + ); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.grantTypes = { ...grantTypes, ...options.extendedGrantTypes }; + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; + this.requireClientAuthentication = + options.requireClientAuthentication || {}; + this.alwaysIssueNewRefreshToken = + options.alwaysIssueNewRefreshToken !== false; + } + + /** + * Token Handler. + */ + + async handle(request: Request, response: Response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.method !== 'POST') { + throw new InvalidRequestError('Invalid request: method must be POST'); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + + // Extend model object with request + this.model.request = request; + + try { + const client = await this.getClient(request, response); + const data = await this.handleGrantType(request, client); + const model = new TokenModel(data, { + allowExtendedTokenAttributes: this.allowExtendedTokenAttributes, + }); + const tokenType = this.getTokenType(model); + this.updateSuccessResponse(response, tokenType); + + return data; + } catch (e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + this.updateErrorResponse(response, e); + throw e; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request, response) { + const credentials = this.getClientCredentials(request); + const grantType = request.body.grant_type; + + if (!credentials.clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if ( + this.isClientAuthenticationRequired(grantType) && + !credentials.clientSecret + ) { + throw new InvalidRequestError('Missing parameter: `client_secret`'); + } + + if (!is.vschar(credentials.clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + if (credentials.clientSecret && !is.vschar(credentials.clientSecret)) { + throw new InvalidRequestError('Invalid parameter: `client_secret`'); + } + try { + const client = await this.model.getClient( + credentials.clientId, + credentials.clientSecret, + ); + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if (!client.grants) { + throw new ServerError('Server error: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError('Server error: `grants` must be an array'); + } + + return client; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if (e instanceof InvalidClientError && request.get('authorization')) { + response.set('WWW-Authenticate', 'Basic realm="Service"'); + + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + } + } + + /** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + getClientCredentials(request: Request) { + const credentials = auth(request as any); + const grantType = request.body.grant_type; + + if (credentials) { + return { + clientId: credentials.name, + clientSecret: credentials.pass, + }; + } + + if (request.body.client_id && request.body.client_secret) { + return { + clientId: request.body.client_id, + clientSecret: request.body.client_secret, + }; + } + + if ( + !this.isClientAuthenticationRequired(grantType) && + request.body.client_id + ) { + return { clientId: request.body.client_id }; + } + + throw new InvalidClientError( + 'Invalid client: cannot retrieve client credentials', + ); + } + + /** + * Handle grant type. + */ + + async handleGrantType(request: Request, client: Client) { + const grantType = request.body.grant_type; + + if (!grantType) { + throw new InvalidRequestError('Missing parameter: `grant_type`'); + } + + if (!is.nchar(grantType) && !is.uri(grantType)) { + throw new InvalidRequestError('Invalid parameter: `grant_type`'); + } + + if (!hasOwnProperty(this.grantTypes, grantType)) { + throw new UnsupportedGrantTypeError( + 'Unsupported grant type: `grant_type` is invalid', + ); + } + + if (!client.grants.includes(grantType)) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + const accessTokenLifetime = this.getAccessTokenLifetime(client); + const refreshTokenLifetime = this.getRefreshTokenLifetime(client); + const GrantType = this.grantTypes[grantType]; + + const options = { + accessTokenLifetime, + model: this.model, + refreshTokenLifetime, + alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, + }; + + return new GrantType(options).handle(request, client); + } + + /** + * Get access token lifetime. + */ + + getAccessTokenLifetime(client: Client) { + return client.accessTokenLifetime || this.accessTokenLifetime; + } + + /** + * Get refresh token lifetime. + */ + + getRefreshTokenLifetime(client: Client) { + return client.refreshTokenLifetime || this.refreshTokenLifetime; + } + + /** + * Get token type. + */ + + getTokenType(model: any) { + return new BearerTokenType( + model.accessToken, + model.accessTokenLifetime, + model.refreshToken, + model.scope, + model.customAttributes, + ); + } + + /** + * Update response when a token is generated. + */ + + updateSuccessResponse(response: Response, tokenType: BearerTokenType) { + response.body = tokenType.valueOf(); + + response.set('Cache-Control', 'no-store'); + response.set('Pragma', 'no-cache'); + } + + /** + * Update response when an error is thrown. + */ + + updateErrorResponse(response: Response, error: OAuthError) { + response.body = { + error: error.name, + error_description: error.message, + }; + + response.status = error.code; + } + + /** + * Given a grant type, check if client authentication is required. + */ + isClientAuthenticationRequired(grantType: string) { + if (Object.keys(this.requireClientAuthentication).length > 0) { + return typeof this.requireClientAuthentication[grantType] !== 'undefined' + ? this.requireClientAuthentication[grantType] + : true; + } + + return true; + } +} diff --git a/lib/interfaces/authorization-code.interface.ts b/lib/interfaces/authorization-code.interface.ts new file mode 100644 index 000000000..a1f781607 --- /dev/null +++ b/lib/interfaces/authorization-code.interface.ts @@ -0,0 +1,14 @@ +import { Client, User } from '.'; + +/** + * An interface representing the authorization code and associated data. + */ +export interface AuthorizationCode { + authorizationCode: string; + expiresAt: Date; + redirectUri: string; + scope?: string; + client: Client; + user: User; + [key: string]: any; +} diff --git a/lib/interfaces/client.interface.ts b/lib/interfaces/client.interface.ts new file mode 100644 index 000000000..dcdb074fe --- /dev/null +++ b/lib/interfaces/client.interface.ts @@ -0,0 +1,11 @@ +/** + * An interface representing the client and associated data + */ +export interface Client { + id: string; + redirectUris?: string | string[]; + grants: string | string[]; + accessTokenLifetime?: number; + refreshTokenLifetime?: number; + [key: string]: any; +} diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts new file mode 100644 index 000000000..a5cb2aeac --- /dev/null +++ b/lib/interfaces/index.ts @@ -0,0 +1,6 @@ +export { AuthorizationCode } from './authorization-code.interface'; +export { Client } from './client.interface'; +export { Model } from './model.interface'; +export { RefreshToken } from './refresh-token.interface'; +export { Token } from './token.interface'; +export { User } from './user.interface'; diff --git a/lib/interfaces/model.interface.ts b/lib/interfaces/model.interface.ts new file mode 100644 index 000000000..14e880e67 --- /dev/null +++ b/lib/interfaces/model.interface.ts @@ -0,0 +1,175 @@ +import { AuthorizationCode, Client, RefreshToken, Token, User } from '.'; +import { Request } from '../request'; + +export interface BaseModel { + request: Request; + /** + * Invoked to generate a new access token. + * + */ + generateAccessToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve a client using a client id or a + * client id/client secret combination, depending on the grant type. + * + */ + getClient(clientId: string, clientSecret?: string): Promise; + + /** + * Invoked to save an access token and optionally a refresh token, depending on the grant type. + * + */ + saveToken(token: Token, client: Client, user: User): Promise; +} + +export interface RequestAuthenticationModel { + /** + * Invoked to retrieve an existing access token previously saved through Model#saveToken(). + * + */ + getAccessToken(accessToken: string): Promise; + + /** + * Invoked during request authentication to check if + * the provided access token was authorized the requested scopes. + * + */ + verifyScope(token: Token, scope: string): Promise; +} + +export interface AuthorizationCodeModel + extends BaseModel, + RequestAuthenticationModel { + /** + * Invoked to generate a new refresh token. + * + */ + generateRefreshToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to generate a new authorization code. + * + */ + generateAuthorizationCode?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve an existing authorization + * code previously saved through Model#saveAuthorizationCode(). + * + */ + getAuthorizationCode(authorizationCode: string): Promise; + + /** + * Invoked to save an authorization code. + * + */ + saveAuthorizationCode( + code: AuthorizationCode, + client: Client, + user: User, + ): Promise; + + /** + * Invoked to revoke an authorization code. + * + */ + revokeAuthorizationCode(code: AuthorizationCode): Promise; + + /** + * Invoked to check if the requested scope is + * valid for a particular client/user combination. + * + */ + validateScope?(user: User, client: Client, scope: string): Promise; +} + +export interface PasswordModel extends BaseModel, RequestAuthenticationModel { + /** + * Invoked to generate a new refresh token. + * + */ + generateRefreshToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve a user using a + * username/password combination. + * + */ + getUser(username: string, password: string): Promise; + + /** + * Invoked to check if the requested scope + * is valid for a particular client/user combination. + * + */ + validateScope?(user: User, client: Client, scope: string): Promise; +} + +export interface RefreshTokenModel + extends BaseModel, + RequestAuthenticationModel { + /** + * Invoked to generate a new refresh token. + * + */ + generateRefreshToken?( + client: Client, + user: User, + scope: string, + ): Promise; + + /** + * Invoked to retrieve an existing refresh token previously saved through Model#saveToken(). + * + */ + getRefreshToken(refreshToken: string): Promise; + + /** + * Invoked to revoke a refresh token. + * + */ + revokeToken(token: RefreshToken | Token): Promise; +} + +export interface ClientCredentialsModel + extends BaseModel, + RequestAuthenticationModel { + /** + * Invoked to retrieve the user associated with the specified client. + * + */ + getUserFromClient(client: Client): Promise; + + /** + * Invoked to check if the requested scope is valid for a particular client/user combination. + * + */ + validateScope?(user: User, client: Client, scope: string): Promise; +} + +export interface ExtensionModel extends BaseModel, RequestAuthenticationModel {} + +export interface Model + extends BaseModel, + RequestAuthenticationModel, + AuthorizationCodeModel, + PasswordModel, + RefreshTokenModel, + ClientCredentialsModel {} diff --git a/lib/interfaces/refresh-token.interface.ts b/lib/interfaces/refresh-token.interface.ts new file mode 100644 index 000000000..71801f87d --- /dev/null +++ b/lib/interfaces/refresh-token.interface.ts @@ -0,0 +1,13 @@ +import { Client, User } from '.'; + +/** + * An interface representing the refresh token and associated data. + */ +export interface RefreshToken { + refreshToken: string; + refreshTokenExpiresAt?: Date; + scope?: string; + client: Client; + user: User; + [key: string]: any; +} diff --git a/lib/interfaces/token.interface.ts b/lib/interfaces/token.interface.ts new file mode 100644 index 000000000..ec696e687 --- /dev/null +++ b/lib/interfaces/token.interface.ts @@ -0,0 +1,15 @@ +import { Client, User } from '.'; + +/** + * An interface representing the token(s) and associated data. + */ +export interface Token { + accessToken: string; + accessTokenExpiresAt?: Date; + refreshToken?: string; + refreshTokenExpiresAt?: Date; + scope?: string; + client: Client; + user: User; + [key: string]: any; +} diff --git a/lib/interfaces/user.interface.ts b/lib/interfaces/user.interface.ts new file mode 100644 index 000000000..dc5afd5b0 --- /dev/null +++ b/lib/interfaces/user.interface.ts @@ -0,0 +1,7 @@ +/** + * An interface representing the user. + * A user object is completely transparent to oauth2-server and is simply used as input to model functions. + */ +export interface User { + [key: string]: any; +} diff --git a/lib/models/index.ts b/lib/models/index.ts new file mode 100644 index 000000000..34c69db1f --- /dev/null +++ b/lib/models/index.ts @@ -0,0 +1 @@ +export { TokenModel } from './token-model'; diff --git a/lib/models/token-model.js b/lib/models/token-model.js deleted file mode 100644 index c6bc3f8d4..000000000 --- a/lib/models/token-model.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); - -/** - * Constructor. - */ - -var modelAttributes = ['accessToken', 'accessTokenExpiresAt', 'refreshToken', 'refreshTokenExpiresAt', 'scope', 'client', 'user']; - -function TokenModel(data, options) { - data = data || {}; - - if (!data.accessToken) { - throw new InvalidArgumentError('Missing parameter: `accessToken`'); - } - - if (!data.client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - if (!data.user) { - throw new InvalidArgumentError('Missing parameter: `user`'); - } - - if (data.accessTokenExpiresAt && !(data.accessTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresAt`'); - } - - if (data.refreshTokenExpiresAt && !(data.refreshTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresAt`'); - } - - this.accessToken = data.accessToken; - this.accessTokenExpiresAt = data.accessTokenExpiresAt; - this.client = data.client; - this.refreshToken = data.refreshToken; - this.refreshTokenExpiresAt = data.refreshTokenExpiresAt; - this.scope = data.scope; - this.user = data.user; - - if (options && options.allowExtendedTokenAttributes) { - this.customAttributes = {}; - - for (var key in data) { - if (data.hasOwnProperty(key) && (modelAttributes.indexOf(key) < 0)) { - this.customAttributes[key] = data[key]; - } - } - } - - if(this.accessTokenExpiresAt) { - this.accessTokenLifetime = Math.floor((this.accessTokenExpiresAt - new Date()) / 1000); - } -} - -/** - * Export constructor. - */ - -module.exports = TokenModel; diff --git a/lib/models/token-model.ts b/lib/models/token-model.ts new file mode 100755 index 000000000..e5104901f --- /dev/null +++ b/lib/models/token-model.ts @@ -0,0 +1,82 @@ +import { MILLISECONDS_PER_SECOND } from '../constants'; +import { InvalidArgumentError } from '../errors'; +import { Client, Token, User } from '../interfaces'; +import { hasOwnProperty } from '../utils/fn'; + +const modelAttributes = [ + 'accessToken', + 'accessTokenExpiresAt', + 'client', + 'refreshToken', + 'refreshTokenExpiresAt', + 'scope', + 'user', +]; + +export class TokenModel implements Token { + accessToken: string; + accessTokenExpiresAt?: Date; + refreshToken?: string; + refreshTokenExpiresAt?: Date; + scope?: string; + client: Client; + user: User; + customAttributes: {}; + accessTokenLifetime: number; + constructor(data: any = {}, options: any = {}) { + if (!data.accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } + + if (!data.client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + if (!data.user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } + + if ( + data.accessTokenExpiresAt && + !(data.accessTokenExpiresAt instanceof Date) + ) { + throw new InvalidArgumentError( + 'Invalid parameter: `accessTokenExpiresAt`', + ); + } + + if ( + data.refreshTokenExpiresAt && + !(data.refreshTokenExpiresAt instanceof Date) + ) { + throw new InvalidArgumentError( + 'Invalid parameter: `refreshTokenExpiresAt`', + ); + } + + this.accessToken = data.accessToken; + this.accessTokenExpiresAt = data.accessTokenExpiresAt; + this.client = data.client; + this.refreshToken = data.refreshToken; + this.refreshTokenExpiresAt = data.refreshTokenExpiresAt; + this.scope = data.scope; + this.user = data.user; + + if (options && options.allowExtendedTokenAttributes) { + this.customAttributes = {}; + + for (const key of Object.keys(data)) { + if (hasOwnProperty(data, key) && modelAttributes.indexOf(key) < 0) { + this.customAttributes[key] = data[key]; + } + } + } + + if (this.accessTokenExpiresAt) { + this.accessTokenLifetime = Math.floor( + (this.accessTokenExpiresAt.getTime() - new Date().getTime()) / + MILLISECONDS_PER_SECOND, + ); + } + } +} diff --git a/lib/request.js b/lib/request.js deleted file mode 100644 index 00b879586..000000000 --- a/lib/request.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('./errors/invalid-argument-error'); -var typeis = require('type-is'); - -/** - * Constructor. - */ - -function Request(options) { - options = options || {}; - - if (!options.headers) { - throw new InvalidArgumentError('Missing parameter: `headers`'); - } - - if (!options.method) { - throw new InvalidArgumentError('Missing parameter: `method`'); - } - - if (!options.query) { - throw new InvalidArgumentError('Missing parameter: `query`'); - } - - this.body = options.body || {}; - this.headers = {}; - this.method = options.method; - this.query = options.query; - - // Store the headers in lower case. - for (var field in options.headers) { - if (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 (Object.prototype.hasOwnProperty.call(options, property) && !this[property]) { - this[property] = options[property]; - } - } -} - -/** - * Get a request header. - */ - -Request.prototype.get = function(field) { - return this.headers[field.toLowerCase()]; -}; - -/** - * Check if the content-type matches any of the given mime type. - */ - -Request.prototype.is = function(types) { - if (!Array.isArray(types)) { - types = [].slice.call(arguments); - } - - return typeis(this, types) || false; -}; - -/** - * Export constructor. - */ - -module.exports = Request; diff --git a/lib/request.ts b/lib/request.ts new file mode 100755 index 000000000..5bfdfe467 --- /dev/null +++ b/lib/request.ts @@ -0,0 +1,77 @@ +import * as typeis from 'type-is'; +import { InvalidArgumentError } from './errors'; +import { hasOwnProperty } from './utils/fn'; + +export class Request { + body: any; + headers: any; + method: string; + query: any; + constructor( + options: { + body: any; + headers: any; + method: string; + query: any; + [key: string]: any; + } = {} as any, + ) { + if (!options.headers) { + throw new InvalidArgumentError('Missing parameter: `headers`'); + } + + if (!options.method) { + throw new InvalidArgumentError('Missing parameter: `method`'); + } + + if (typeof options.method !== 'string') { + throw new InvalidArgumentError('Invalid parameter: `method`'); + } + + if (!options.query) { + throw new InvalidArgumentError('Missing parameter: `query`'); + } + + this.body = options.body || {}; + this.headers = {}; + this.method = options.method.toUpperCase(); + this.query = options.query; + + // Store the headers in lower case. + for (const field of Object.keys(options.headers)) { + if (hasOwnProperty(options.headers, field)) { + this.headers[field.toLowerCase()] = options.headers[field]; + } + } + + // Store additional properties of the request object passed in + for (const property of Object.keys(options)) { + if (hasOwnProperty(options, property) && !this[property]) { + this[property] = options[property]; + } + } + } + + /** + * Get a request header. + */ + + get(field: string) { + return this.headers[field.toLowerCase()]; + } + + /** + * Check if the content-type matches any of the given mime type. + */ + public is(args: string[]): string | false; + public is(...args: string[]): string | false; + + is(...args) { + let types = args; + if (Array.isArray(types[0])) { + types = types[0]; + } + + return typeis(this as any, types) || false; + } +} diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js deleted file mode 100644 index f0dd8a141..000000000 --- a/lib/response-types/code-response-type.js +++ /dev/null @@ -1,157 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var tokenUtil = require('../utils/token-util'); -var Promise = require('bluebird'); - -/** - * Constructor. - */ - -function CodeResponseType(options) { - options = options || {}; - - if (!options.authorizationCodeLifetime) { - throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.saveAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); - } - - this.code = 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. - */ - -CodeResponseType.prototype.buildRedirectUri = function(redirectUri) { - if (!redirectUri) { - throw new InvalidArgumentError('Missing parameter: `redirectUri`'); - } - - 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`'); - } - - redirectUri.query = redirectUri.query || {}; - redirectUri.query[key] = value; - - return redirectUri; -}; - -/** - * Export constructor. - */ - -module.exports = CodeResponseType; diff --git a/lib/response-types/code-response-type.ts b/lib/response-types/code-response-type.ts new file mode 100755 index 000000000..0d1b93e31 --- /dev/null +++ b/lib/response-types/code-response-type.ts @@ -0,0 +1,165 @@ +import { MILLISECONDS_PER_SECOND } from '../constants'; +import { InvalidArgumentError } from '../errors'; +import { AuthorizationCode, Client, Model, User } from '../interfaces'; +import { Request } from '../request'; +import * as tokenUtil from '../utils/token-util'; +export class CodeResponseType { + code: any; + authorizationCodeLifetime: number; + model: Model; + constructor(options: any = {}) { + if (!options.authorizationCodeLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `authorizationCodeLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.saveAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveAuthorizationCode()`', + ); + } + + this.code = undefined; + this.authorizationCodeLifetime = options.authorizationCodeLifetime; + this.model = options.model; + } + + /** + * Handle code response type. + */ + + async handle( + request: Request, + client: Client, + user: User, + uri: string, + scope: string, + ) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + if (!user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } + + if (!uri) { + throw new InvalidArgumentError('Missing parameter: `uri`'); + } + + const authorizationCode = await this.generateAuthorizationCode( + client, + user, + scope, + ); + const expiresAt = this.getAuthorizationCodeExpiresAt(client); + + const code = await this.saveAuthorizationCode( + authorizationCode, + expiresAt, + scope, + client, + uri, + user, + ); + this.code = code.authorizationCode; + + return code; + } + + /** + * Get authorization code expiration date. + */ + + getAuthorizationCodeExpiresAt(client: Client) { + const authorizationCodeLifetime = this.getAuthorizationCodeLifetime(client); + + return new Date( + Date.now() + authorizationCodeLifetime * MILLISECONDS_PER_SECOND, + ); + } + + /** + * Get authorization code lifetime. + */ + + getAuthorizationCodeLifetime(client: Client) { + return client.authorizationCodeLifetime || this.authorizationCodeLifetime; + } + + /** + * Save authorization code. + */ + + async saveAuthorizationCode( + authorizationCode: string, + expiresAt: Date, + scope: string, + client: Client, + redirectUri: any, + user: User, + ) { + const code = { + authorizationCode, + expiresAt, + redirectUri, + scope, + } as AuthorizationCode; + + return this.model.saveAuthorizationCode(code, client, user); + } + + /** + * Generate authorization code. + */ + + async generateAuthorizationCode(client: Client, user: User, scope: string) { + if (this.model.generateAuthorizationCode) { + return this.model.generateAuthorizationCode(client, user, scope); + } + + return tokenUtil.GenerateRandomToken(); + } + + /** + * Build redirect uri. + */ + + buildRedirectUri(redirectUri: any) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + redirectUri.search = undefined; + + return this.setRedirectUriParam(redirectUri, 'code', this.code); + } + + /** + * Set redirect uri parameter. + */ + + setRedirectUriParam(redirectUri: any, key: string, value: string) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + if (!key) { + throw new InvalidArgumentError('Missing parameter: `key`'); + } + + redirectUri.query = redirectUri.query || {}; + redirectUri.query[key] = value; + + return redirectUri; + } +} diff --git a/lib/response-types/index.ts b/lib/response-types/index.ts new file mode 100644 index 000000000..9866921eb --- /dev/null +++ b/lib/response-types/index.ts @@ -0,0 +1,2 @@ +export { CodeResponseType } from './code-response-type'; +export { TokenResponseType } from './token-response-type'; diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js deleted file mode 100644 index 92352150e..000000000 --- a/lib/response-types/token-response-type.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); -var ImplicitGrantType = require('../grant-types/implicit-grant-type'); -var Promise = require('bluebird'); - -/** - * Constructor. - */ - -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. - */ - -module.exports = TokenResponseType; diff --git a/lib/response-types/token-response-type.ts b/lib/response-types/token-response-type.ts new file mode 100755 index 000000000..8901a3228 --- /dev/null +++ b/lib/response-types/token-response-type.ts @@ -0,0 +1,97 @@ +import { InvalidArgumentError } from '../errors'; +import { ImplicitGrantType } from '../grant-types'; +import { Client, Model, User } from '../interfaces'; +import { Request } from '../request'; + +export class TokenResponseType { + accessToken: string; + accessTokenLifetime: number; + model: Model; + constructor(options: any = {}) { + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + this.accessToken = undefined; + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; + } + + /** + * Handle token response type. + */ + + async handle( + request: Request, + client: Client, + user: User, + uri: string, + scope: string, + ) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const accessTokenLifetime = this.getAccessTokenLifetime(client); + + const options = { + user, + scope, + model: this.model, + accessTokenLifetime, + }; + + const grantType = new ImplicitGrantType(options); + const token = await grantType.handle(request, client); + this.accessToken = token.accessToken; + + return token; + } + + /** + * Get access token lifetime. + */ + + getAccessTokenLifetime(client: Client) { + return client.accessTokenLifetime || this.accessTokenLifetime; + } + + /** + * Build redirect uri. + */ + + buildRedirectUri(redirectUri: any) { + return this.setRedirectUriParam( + redirectUri, + 'access_token', + this.accessToken, + ); + } + + /** + * Set redirect uri parameter. + */ + + setRedirectUriParam(redirectUri: any, key: string, value: any) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + if (!key) { + throw new InvalidArgumentError('Missing parameter: `key`'); + } + + redirectUri.hash = redirectUri.hash || ''; + redirectUri.hash += `${ + redirectUri.hash ? '&' : '' + }${key}=${encodeURIComponent(value)}`; + + return redirectUri; + } +} diff --git a/lib/response.js b/lib/response.js deleted file mode 100644 index 76fe854ab..000000000 --- a/lib/response.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -/** - * Constructor. - */ - -function Response(options) { - options = options || {}; - - this.body = options.body || {}; - this.headers = {}; - this.status = 200; - - // Store the headers in lower case. - for (var field in options.headers) { - if (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 (Object.prototype.hasOwnProperty.call(options, property) && !this[property]) { - this[property] = options[property]; - } - } -} - -/** - * Get a response header. - */ - -Response.prototype.get = function(field) { - return this.headers[field.toLowerCase()]; -}; - -/** - * Redirect response. - */ - -Response.prototype.redirect = function(url) { - this.set('Location', url); - this.status = 302; -}; - -/** - * Set a response header. - */ - -Response.prototype.set = function(field, value) { - this.headers[field.toLowerCase()] = value; -}; - -/** - * Export constructor. - */ - -module.exports = Response; diff --git a/lib/response.ts b/lib/response.ts new file mode 100755 index 000000000..3128f10a0 --- /dev/null +++ b/lib/response.ts @@ -0,0 +1,51 @@ +import { hasOwnProperty } from './utils/fn'; + +export class Response { + body: any; + headers: any; + status: number; + constructor(options: any = {}) { + this.body = options.body || {}; + this.headers = {}; + this.status = 200; // OK + + // Store the headers in lower case. + for (const field of Object.keys(options.headers || {})) { + if (hasOwnProperty(options.headers, field)) { + this.headers[field.toLowerCase()] = options.headers[field]; + } + } + + // Store additional properties of the response object passed in. + for (const property of Object.keys(options)) { + if (hasOwnProperty(options, property) && !this[property]) { + this[property] = options[property]; + } + } + } + + /** + * Get a response header. + */ + + get(field: string) { + return this.headers[field.toLowerCase()]; + } + + /** + * Redirect response. + */ + + redirect(url: string) { + this.set('Location', url); + this.status = 302; // Found + } + + /** + * Set a response header. + */ + + set(field: string, value: string) { + this.headers[field.toLowerCase()] = value; + } +} diff --git a/lib/server.js b/lib/server.js deleted file mode 100644 index 3a4901c70..000000000 --- a/lib/server.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var _ = require('lodash'); -var AuthenticateHandler = require('./handlers/authenticate-handler'); -var AuthorizeHandler = require('./handlers/authorize-handler'); -var InvalidArgumentError = require('./errors/invalid-argument-error'); -var TokenHandler = require('./handlers/token-handler'); -var RevokeHandler = require('./handlers/revoke-handler'); - -/** - * Constructor. - */ - -function OAuth2Server(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - this.options = options; -} - -/** - * Authenticate a token. - */ - -OAuth2Server.prototype.authenticate = function(request, response, options, callback) { - if (typeof options === 'string') { - options = {scope: options}; - } - - options = _.assign({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - allowBearerTokensInQueryString: false - }, this.options, options); - - return new AuthenticateHandler(options) - .handle(request, response) - .nodeify(callback); -}; - -/** - * Authorize a request. - */ - -OAuth2Server.prototype.authorize = function(request, response, options, callback) { - options = _.assign({ - allowEmptyState: false, - accessTokenLifetime: 60 * 60, // 1 hour. - authorizationCodeLifetime: 5 * 60 // 5 minutes. - }, this.options, options); - - return new AuthorizeHandler(options) - .handle(request, response) - .nodeify(callback); -}; - -/** - * Create a token. - */ - -OAuth2Server.prototype.token = function(request, response, options, callback) { - options = _.assign({ - accessTokenLifetime: 60 * 60, // 1 hour. - refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. - allowExtendedTokenAttributes: false, - requireClientAuthentication: {} // defaults to true for all grant types - }, this.options, options); - - return new TokenHandler(options) - .handle(request, response) - .nodeify(callback); -}; - -/** - * 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. - */ - -module.exports = OAuth2Server; diff --git a/lib/server.ts b/lib/server.ts new file mode 100755 index 000000000..3e81e441d --- /dev/null +++ b/lib/server.ts @@ -0,0 +1,100 @@ +import { HOUR, MINUTE, SECOND, WEEK } from './constants'; +import { InvalidArgumentError } from './errors'; +import { + AuthenticateHandler, + AuthorizeHandler, + RevokeHandler, + TokenHandler, +} from './handlers'; +import { Request } from './request'; +import { Response } from './response'; + +export class OAuth2Server { + options: any; + constructor(options: any = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + this.options = options; + } + + /** + * Authenticate a token. + */ + authenticate( + request: Request, + response?: Response, + scope?: string, + ): Promise; + authenticate( + request: Request, + response?: Response, + // tslint:disable-next-line:unified-signatures + options?: any, + ): Promise; + + async authenticate( + request: Request, + response?: Response, + options?: string | any, + ) { + let opt = options; + if (typeof opt === 'string') { + opt = { scope: opt }; + } + + opt = { + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + allowBearerTokensInQueryString: false, + ...this.options, + ...opt, + }; + + return new AuthenticateHandler(opt).handle(request, response); + } + + /** + * Authorize a request. + */ + + async authorize(request: Request, response: Response, options?: any) { + const opts = { + allowEmptyState: false, + accessTokenLifetime: HOUR / SECOND, + authorizationCodeLifetime: (MINUTE * 5) / SECOND, + ...this.options, + ...options, + }; + + return new AuthorizeHandler(opts).handle(request, response); + } + + /** + * Create a token. + */ + + async token(request: Request, response: Response, options?: any) { + const opts = { + accessTokenLifetime: HOUR / SECOND, // 1 hour in seconds. + refreshTokenLifetime: (WEEK * 2) / SECOND, // 2 weeks. + allowExtendedTokenAttributes: false, + requireClientAuthentication: {}, + ...this.options, + ...options, + }; + + return new TokenHandler(opts).handle(request, response); + } + + /** + * Revoke a token. + */ + + async revoke(request: Request, response: Response, options: any) { + const opt = { ...this.options, ...options }; + + return new RevokeHandler(opt).handle(request, response); + } +} diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js deleted file mode 100644 index 9124cb2e8..000000000 --- a/lib/token-types/bearer-token-type.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../errors/invalid-argument-error'); - -/** - * Constructor. - */ - -function BearerTokenType(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes) { - if (!accessToken) { - throw new InvalidArgumentError('Missing parameter: `accessToken`'); - } - - this.accessToken = accessToken; - this.accessTokenLifetime = accessTokenLifetime; - this.refreshToken = refreshToken; - this.scope = scope; - - if (customAttributes) { - this.customAttributes = customAttributes; - } -} - -/** - * Retrieve the value representation. - */ - -BearerTokenType.prototype.valueOf = function() { - var object = { - access_token: this.accessToken, - token_type: 'Bearer' - }; - - if (this.accessTokenLifetime) { - object.expires_in = this.accessTokenLifetime; - } - - if (this.refreshToken) { - object.refresh_token = this.refreshToken; - } - - if (this.scope) { - object.scope = this.scope; - } - - for (var key in this.customAttributes) { - if (this.customAttributes.hasOwnProperty(key)) { - object[key] = this.customAttributes[key]; - } - } - return object; -}; - -/** - * Export constructor. - */ - -module.exports = BearerTokenType; diff --git a/lib/token-types/bearer-token-type.ts b/lib/token-types/bearer-token-type.ts new file mode 100755 index 000000000..dd08c4e72 --- /dev/null +++ b/lib/token-types/bearer-token-type.ts @@ -0,0 +1,61 @@ +import { InvalidArgumentError } from '../errors'; +import { hasOwnProperty } from '../utils/fn'; + +export class BearerTokenType { + accessToken: string; + accessTokenLifetime: number; + refreshToken: string; + scope: string; + customAttributes: any; + constructor( + accessToken: string, + accessTokenLifetime: number, + refreshToken: string, + scope: string, + customAttributes: any, + ) { + if (!accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } + + this.accessToken = accessToken; + this.accessTokenLifetime = accessTokenLifetime; + this.refreshToken = refreshToken; + this.scope = scope; + + if (customAttributes) { + this.customAttributes = customAttributes; + } + } + + /** + * Retrieve the value representation. + */ + + valueOf() { + const object: any = { + access_token: this.accessToken, + token_type: 'Bearer', + }; + + if (this.accessTokenLifetime) { + object.expires_in = this.accessTokenLifetime; + } + + if (this.refreshToken) { + object.refresh_token = this.refreshToken; + } + + if (this.scope) { + object.scope = this.scope; + } + + for (const key of Object.keys(this.customAttributes || {})) { + if (hasOwnProperty(this.customAttributes, key)) { + object[key] = this.customAttributes[key]; + } + } + + return object; + } +} diff --git a/lib/token-types/index.ts b/lib/token-types/index.ts new file mode 100644 index 000000000..7fa71bce4 --- /dev/null +++ b/lib/token-types/index.ts @@ -0,0 +1,2 @@ +export { BearerTokenType } from './bearer-token-type'; +export { MacTokenType } from './mac-token-type'; diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js deleted file mode 100644 index 9fdc600c6..000000000 --- a/lib/token-types/mac-token-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ServerError = require('../errors/server-error'); - -/** - * Constructor. - */ - -function MacTokenType() { - throw new ServerError('Not implemented.'); -} - -/** - * Export constructor. - */ - -module.exports = MacTokenType; diff --git a/lib/token-types/mac-token-type.ts b/lib/token-types/mac-token-type.ts new file mode 100755 index 000000000..8210f7c0d --- /dev/null +++ b/lib/token-types/mac-token-type.ts @@ -0,0 +1,8 @@ +import { ServerError } from '../errors'; + +// tslint:disable-next-line:no-unnecessary-class +export class MacTokenType { + constructor() { + throw new ServerError('Not implemented.'); + } +} diff --git a/lib/utils/fn.ts b/lib/utils/fn.ts new file mode 100644 index 000000000..ac5200c4d --- /dev/null +++ b/lib/utils/fn.ts @@ -0,0 +1,20 @@ +const identity = (v: any) => v; + +const reverser = (promise: Promise) => + promise.then(v => Promise.reject(v), identity); + +export const oneSuccess = (promises: Array>) => + Promise.all(promises.map(reverser)).then( + e => Promise.reject(AggregateError.from(e)), + identity, + ); + +export const hasOwnProperty = (o: any, k: string) => + Object.prototype.hasOwnProperty.call(o, k); + +export class AggregateError extends Array implements Error { + name = 'AggregateError'; + get message() { + return this.map(e => e.message).join('\n'); + } +} diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js deleted file mode 100644 index 0f73746bb..000000000 --- a/lib/utils/token-util.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var crypto = require('crypto'); -var randomBytes = require('bluebird').promisify(require('crypto').randomBytes); - -/** - * Export `TokenUtil`. - */ - -module.exports = { - - /** - * Generate random token. - */ - - generateRandomToken: function() { - return randomBytes(256).then(function(buffer) { - return crypto - .createHash('sha1') - .update(buffer) - .digest('hex'); - }); - } - -}; diff --git a/lib/utils/token-util.ts b/lib/utils/token-util.ts new file mode 100755 index 000000000..9e9270ed0 --- /dev/null +++ b/lib/utils/token-util.ts @@ -0,0 +1,16 @@ +import { createHash, randomBytes } from 'crypto'; +import { promisify } from 'util'; +const randomBytesPromise = promisify(randomBytes); + +/** + * Generate random token. + */ + +export const GenerateRandomToken = async () => { + const bytesSize = 256; + const buffer = await randomBytesPromise(bytesSize); + + return createHash('sha1') + .update(buffer) + .digest('hex'); +}; diff --git a/lib/validator/is.js b/lib/validator/is.js deleted file mode 100644 index 07af6cb64..000000000 --- a/lib/validator/is.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -/** - * Validation rules. - */ - -var rules = { - NCHAR: /^[\u002D|\u002E|\u005F|\w]+$/, - NQCHAR: /^[\u0021|\u0023-\u005B|\u005D-\u007E]+$/, - NQSCHAR: /^[\u0020-\u0021|\u0023-\u005B|\u005D-\u007E]+$/, - UNICODECHARNOCRLF: /^[\u0009|\u0020-\u007E|\u0080-\uD7FF|\uE000-\uFFFD|\u10000-\u10FFFF]+$/, - URI: /^[a-zA-Z][a-zA-Z0-9+.-]+:/, - VSCHAR: /^[\u0020-\u007E]+$/ -}; - -/** - * Export validation functions. - */ - -module.exports = { - - /** - * Validate if a value matches a unicode character. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - nchar: function(value) { - return rules.NCHAR.test(value); - }, - - /** - * Validate if a value matches a unicode character, including exclamation marks. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - nqchar: function(value) { - return rules.NQCHAR.test(value); - }, - - /** - * Validate if a value matches a unicode character, including exclamation marks and spaces. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - nqschar: function(value) { - return rules.NQSCHAR.test(value); - }, - - /** - * Validate if a value matches a unicode character excluding the carriage - * return and linefeed characters. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - uchar: function(value) { - return rules.UNICODECHARNOCRLF.test(value); - }, - - /** - * Validate if a value matches generic URIs. - * - * @see http://tools.ietf.org/html/rfc3986#section-3 - */ - uri: function(value) { - return rules.URI.test(value); - }, - - /** - * Validate if a value matches against the printable set of unicode characters. - * - * @see https://tools.ietf.org/html/rfc6749#appendix-A - */ - - vschar: function(value) { - return rules.VSCHAR.test(value); - } -}; diff --git a/lib/validator/is.ts b/lib/validator/is.ts new file mode 100755 index 000000000..d9475878a --- /dev/null +++ b/lib/validator/is.ts @@ -0,0 +1,64 @@ +/** + * Validation rules. + */ + +const Rules = { + NCHAR: /^[\u002D|\u002E|\u005F|\w]+$/, + NQCHAR: /^[\u0021|\u0023-\u005B|\u005D-\u007E]+$/, + NQSCHAR: /^[\u0020-\u0021|\u0023-\u005B|\u005D-\u007E]+$/, + UNICODECHARNOCRLF: /^[\u0009|\u0020-\u007E|\u0080-\uD7FF|\uE000-\uFFFD|\u10000-\u10FFFF]+$/, + URI: /^[a-zA-Z][a-zA-Z0-9+.-]+:/, + VSCHAR: /^[\u0020-\u007E]+$/, +}; + +/** + * Export validation functions. + */ + +/** + * Validate if a value matches a unicode character. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const nchar = (value: string) => Rules.NCHAR.test(value); + +/** + * Validate if a value matches a unicode character, including exclamation marks. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const nqchar = (value: string) => Rules.NQCHAR.test(value); + +/** + * Validate if a value matches a unicode character, including exclamation marks and spaces. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const nqschar = (value: string) => Rules.NQSCHAR.test(value); + +/** + * Validate if a value matches a unicode character excluding the carriage + * and linefeed characters. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const uchar = (value: string) => Rules.UNICODECHARNOCRLF.test(value); + +/** + * Validate if a value matches generic URIs. + * + * @see http://tools.ietf.org/html/rfc3986#section-3 + */ +export const uri = (value: string) => Rules.URI.test(value); + +/** + * Validate if a value matches against the printable set of unicode characters. + * + * @see https://tools.ietf.org/html/rfc6749#appendix-A + */ + +export const vschar = (value: string) => Rules.VSCHAR.test(value); diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index 052ce992d..9d7156cac --- a/package-lock.json +++ b/package-lock.json @@ -1,82 +1,128 @@ { "name": "oauth2-server", - "version": "4.0.0-dev.2", + "version": "5.0.0-dev.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, "@sinonjs/commons": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz", - "integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.6.0.tgz", + "integrity": "sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==", "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==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", "dev": true, "requires": { - "samsam": "1.3.0" + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.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==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", "dev": true, "requires": { - "acorn": "^5.0.3" + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" } }, - "ajv": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", - "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", - "dev": true, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/basic-auth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.2.tgz", + "integrity": "sha512-NzkkcC+gkkILWaBi3+/z/3do6Ybk6TWeTqV5zCVXmG2KaBoT5YqlJvfqP44HCyDA+Cu58pp7uKAxy/G58se/TA==", "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" + "@types/node": "*" } }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "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==", + "@types/node": { + "version": "11.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.3.tgz", + "integrity": "sha512-5RzvXVietaB8S4dwDjxjltAOHtTO87fiksjqjWGZih97j6KSrdCDaRfmYMNrgrLM87odGBrsTHAl6N3fLraQaw==" + }, + "@types/sinon": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz", + "integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==", + "dev": true + }, + "@types/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-4zJN5gJH+Km6hA36z8MnOKas6EU0qwxItTXNijYDPuZUsSk4EpIAB56fwnxZIhi3tHx42J7wqNdQTqt49Ar9FQ==" + }, + "@types/type-is": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", + "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", + "requires": { + "@types/node": "*" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true }, "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "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" + } + }, + "arg": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", + "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==", "dev": true }, "argparse": { @@ -88,61 +134,29 @@ "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-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", "dev": true }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", "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" - } - } - } + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true }, "balanced-match": { "version": "1.0.0", @@ -151,18 +165,13 @@ "dev": true }, "basic-auth": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", - "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "5.1.2" } }, - "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", @@ -179,25 +188,28 @@ "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" - } + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -225,73 +237,64 @@ } } }, - "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=", + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "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" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { - "bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, - "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==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "color-name": "1.1.1" + "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "concat-map": { @@ -314,33 +317,27 @@ } }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, - "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=", + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "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" + "object-keys": "^1.0.12" } }, "diff": { @@ -349,193 +346,86 @@ "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==", + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { - "esutils": "^2.0.2" + "is-arrayish": "^0.2.1" } }, - "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==", + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", "dev": true, "requires": { - "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" + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" } }, - "eslint-scope": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", - "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "dev": true, "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, - "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==", + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", "dev": true }, - "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==", + "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 }, - "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=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "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=", + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "locate-path": "^3.0.0" } }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "del": "^2.0.2", - "graceful-fs": "^4.1.2", - "write": "^0.2.1" + "is-buffer": "~2.0.3" } }, "fs.realpath": { @@ -544,10 +434,16 @@ "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=", + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, "glob": { @@ -564,30 +460,10 @@ "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=", + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, "growl": { @@ -596,13 +472,13 @@ "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=", + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "function-bind": "^1.1.1" } }, "has-flag": { @@ -611,31 +487,22 @@ "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=", + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, - "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==", + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, "inflight": { @@ -654,74 +521,60 @@ "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" - } + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, - "is-generator": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", - "integrity": "sha1-wUwhBX7TbjKNuANHlmxpP4hjifM=" + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", "dev": true }, - "is-path-in-cwd": { + "is-date-object": { "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==", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "is-path-inside": "^1.0.0" + "has": "^1.0.1" } }, - "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=", + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", "dev": true, "requires": { - "path-is-inside": "^1.0.1" + "has-symbols": "^1.0.0" } }, - "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", @@ -735,64 +588,86 @@ "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=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" } }, - "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==", + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, - "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=", + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "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==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", "dev": true }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" } }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, "lolex": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz", - "integrity": "sha512-Oo2Si3RMKV3+lV5MsSWplDQFoTClz/24S0MMHYcgGWWmFXr6TMlqcqk/l1GtH+d5wLBwNRiqGnwDRMirtFalJw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, "media-typer": { @@ -800,25 +675,25 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.36.0" + "mime-db": "1.40.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", @@ -844,65 +719,40 @@ } }, "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", "dev": true, "requires": { + "ansi-colors": "3.2.3", "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", + "debug": "3.2.6", "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.2", + "find-up": "3.0.0", + "glob": "7.1.3", "growl": "1.10.5", - "he": "1.1.1", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", "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": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.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=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, "nice-try": { @@ -912,24 +762,85 @@ "dev": true }, "nise": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.4.tgz", - "integrity": "sha512-pxE0c9PzgrUTyhfv5p+5eMIdfU2bLEsq8VQEuE0kxM4zP7SujSar7rk9wpI2F7RyyCEvLyj5O7Is3RER5F36Fg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz", + "integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, "requires": { - "@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" + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" } }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -939,33 +850,44 @@ "wrappy": "1" } }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "p-try": "^2.0.0" } }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "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" + "p-limit": "^2.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=", + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true }, "path-is-absolute": { @@ -974,169 +896,104 @@ "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-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" } }, - "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=", + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { - "pinkie": "^2.0.0" + "pify": "^3.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=", + "pidtree": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", + "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", "dev": true }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "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=", + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, "requires": { - "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=" - } + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" } }, - "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=", + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "dev": true, "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" + "resolve": "^1.1.6" } }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "restore-cursor": { + "require-main-filename": { "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" - } + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, - "rxjs": { - "version": "5.5.11", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", - "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", "dev": true, "requires": { - "symbol-observable": "1.0.1" + "path-parse": "^1.0.6" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "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==", + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, "shebang-command": { @@ -1154,6 +1011,29 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "should": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", @@ -1208,27 +1088,38 @@ "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 + "shx": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.2.tgz", + "integrity": "sha512-aS0mWtW3T2sHAenrSrip2XGv39O9dXIFUqxAEWHEOS1ePtGIBavdPJY1kE2IHl14V/4iCbUiNDPGdyYTtmhSoA==", + "dev": true, + "requires": { + "es6-object-assign": "^1.0.3", + "minimist": "^1.2.0", + "shelljs": "^0.8.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "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==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", "dev": true, "requires": { - "@sinonjs/commons": "^1.0.1", - "@sinonjs/formatio": "^2.0.0", - "@sinonjs/samsam": "^2.0.0", + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", "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" + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" }, "dependencies": { "supports-color": { @@ -1242,15 +1133,54 @@ } } }, - "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==", + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1272,6 +1202,17 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.padend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", + "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.4.3", + "function-bind": "^1.0.2" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -1279,16 +1220,14 @@ "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-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -1296,65 +1235,76 @@ "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==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { - "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" + "has-flag": "^3.0.0" } }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true + "ts-node": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz", + "integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "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 + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, - "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==", + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", "dev": true, "requires": { - "os-tmpdir": "~1.0.2" + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } } }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { - "prelude-ls": "~1.1.2" + "tslib": "^1.8.1" } }, "type-detect": { @@ -1364,21 +1314,28 @@ "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==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "requires": { "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "mime-types": "~2.1.24" } }, - "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==", + "typescript": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { - "punycode": "^2.1.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, "which": { @@ -1390,26 +1347,144 @@ "isexe": "^2.0.0" } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dev": true, "requires": { - "mkdirp": "^0.5.1" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index c4d4828f4..b26328d1f --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oauth2-server", "description": "Complete, framework-agnostic, compliant and well tested module for implementing an OAuth2 Server in node.js", - "version": "4.0.0-dev.2", + "version": "5.0.0-dev.0", "keywords": [ "oauth", "oauth2" @@ -42,32 +42,47 @@ "email": "marco.luethy@gmail.com" } ], - "main": "index.js", - "dependencies": { - "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": { - "eslint": "^5.2.0", - "mocha": "^5.2.0", - "should": "^13.2.3", - "sinon": "^6.1.4" - }, - "license": "MIT", + "main": "index.ts", "engines": { - "node": ">=6.14" + "node": ">=8.10" }, + "license": "MIT", "scripts": { - "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'" + "lint": "tslint -p tsconfig.build.json -c tslint.json", + "lint:all": "tslint -p tsconfig.json -c tslint.json", + "build:clean": "npx shx rm -rf ./dist", + "build": "npx npm-run-all build:clean build:test:*", + "build:test:src": "npx tsc -p tsconfig.json", + "build:test:package": "node ./scripts/build-prod.js", + "build:prod": "npx npm-run-all build:clean build:prod:*", + "build:prod:src": "npx tsc -p tsconfig.build.json", + "build:prod:package": "node ./scripts/build-prod.js", + "test": "npx mocha 'test/**/*.spec.ts' --config 'test/.mocharc.jsonc'" }, "repository": { - "type": "git", - "url": "https://github.com/oauthjs/node-oauth2-server.git" + "url": "https://github.com/oauthjs/node-oauth2-server.git", + "type": "git" + }, + "devDependencies": { + "@types/mocha": "^5.2.7", + "@types/sinon": "^7.5.1", + "mocha": "^6.2.2", + "npm-run-all": "^4.1.5", + "should": "^13.2.3", + "shx": "^0.3.2", + "sinon": "^7.5.0", + "ts-node": "^8.5.4", + "tslint": "^5.20.1", + "typescript": "^3.7.2" + }, + "dependencies": { + "@types/basic-auth": "^1.1.2", + "@types/node": "^11.15.3", + "@types/statuses": "^1.5.0", + "@types/type-is": "^1.6.3", + "basic-auth": "^2.0.1", + "statuses": "^1.5.0", + "tslib": "^1.10.0", + "type-is": "^1.6.18" } } diff --git a/scripts/build-prod.js b/scripts/build-prod.js new file mode 100644 index 000000000..3f2771837 --- /dev/null +++ b/scripts/build-prod.js @@ -0,0 +1,10 @@ +const fs = require('fs'); +const packageJson = require('../package.json'); + +delete packageJson.scripts; +delete packageJson.devDependencies; +packageJson.main = 'index.js'; +fs.writeFileSync( + __dirname + '/../dist/package.json', + JSON.stringify(packageJson, null, 2), +); diff --git a/test/.mocharc.jsonc b/test/.mocharc.jsonc new file mode 100644 index 000000000..a8ff95b9d --- /dev/null +++ b/test/.mocharc.jsonc @@ -0,0 +1,7 @@ +{ + "require": ["ts-node/register", "should", "test/assertions"], + "reporter": "spec", + "slow": 75, + "timeout": 2000, + "ui": "bdd" +} diff --git a/test/assertions.js b/test/assertions.js old mode 100644 new mode 100755 index 6846e3cdf..f057cdeb5 --- a/test/assertions.js +++ b/test/assertions.js @@ -1,17 +1,12 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var should = require('should'); - /** * SHA-1 assertion. */ -should.Assertion.add('sha1', function() { - this.params = { operator: 'to be a valid SHA-1 hash' }; - - this.obj.should.match(/^[a-f0-9]{40}$/i); -}, true); +should.Assertion.add( + 'sha1', + function() { + this.params = { operator: 'to be a valid SHA-1 hash' }; + this.obj.should.match(/^[a-f0-9]{40}$/i); + }, + true, +); diff --git a/test/integration/grant-types/abstract-grant-type.spec.ts b/test/integration/grant-types/abstract-grant-type.spec.ts new file mode 100755 index 000000000..2bde67443 --- /dev/null +++ b/test/integration/grant-types/abstract-grant-type.spec.ts @@ -0,0 +1,231 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { AbstractGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; +/** + * Test `AbstractGrantType` integration. + */ +describe('AbstractGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.accessTokenLifetime` is missing', () => { + try { + new AbstractGrantType(); + should.fail('no error was thrown', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', () => { + try { + new AbstractGrantType({ accessTokenLifetime: 123 }); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should set the `accessTokenLifetime`', () => { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + }); + + grantType.accessTokenLifetime.should.equal(123); + }); + + it('should set the `model`', () => { + const model = {}; + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.model.should.equal(model); + }); + + it('should set the `refreshTokenLifetime`', () => { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + grantType.refreshTokenLifetime.should.equal(456); + }); + }); + + describe('generateAccessToken()', () => { + it('should return an access token', async () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + try { + const data: any = await handler.generateAccessToken(); + data.should.be.a.sha1(); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const model = { + generateAccessToken() { + return Promise.resolve({}); + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateAccessToken().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + generateAccessToken() { + return {}; + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateAccessToken().should.be.an.instanceOf(Promise); + }); + }); + + describe('generateRefreshToken()', () => { + it('should return a refresh token', async () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + try { + const data: any = await handler.generateRefreshToken(); + data.should.be.a.sha1(); + } catch (error) { + should.fail('should.fail fail', error.message); + } + }); + + it('should support promises', () => { + const model = { + generateRefreshToken() { + return Promise.resolve({}); + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateRefreshToken().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + generateRefreshToken() { + return {}; + }, + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + handler.generateRefreshToken().should.be.an.instanceOf(Promise); + }); + }); + + describe('getAccessTokenExpiresAt()', () => { + it('should return a date', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe('getRefreshTokenExpiresAt()', () => { + it('should return a refresh token', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe('getScope()', () => { + it('should throw an error if `scope` is invalid', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getScope(request); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + } + }); + + it('should allow the `scope` to be `undefined`', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + should.not.exist(handler.getScope(request)); + }); + + it('should return the scope', () => { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getScope(request).should.equal('foo'); + }); + }); +}); diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js deleted file mode 100644 index 6e6258b29..000000000 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ /dev/null @@ -1,174 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var should = require('should'); - -/** - * Test `AbstractGrantType` integration. - */ - -describe('AbstractGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.accessTokenLifetime` is missing', function() { - try { - new AbstractGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new AbstractGrantType({ accessTokenLifetime: 123 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should set the `accessTokenLifetime`', function() { - var grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {} }); - - grantType.accessTokenLifetime.should.equal(123); - }); - - it('should set the `model`', function() { - var model = {}; - var grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.model.should.equal(model); - }); - - it('should set the `refreshTokenLifetime`', function() { - var grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - grantType.refreshTokenLifetime.should.equal(456); - }); - }); - - describe('generateAccessToken()', function() { - it('should return an access token', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateAccessToken() - .then(function(data) { - data.should.be.a.sha1(); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - generateAccessToken: function() { - return Promise.resolve({}); - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - generateAccessToken: function() { - return {}; - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); - }); - }); - - describe('generateRefreshToken()', function() { - it('should return a refresh token', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - return handler.generateRefreshToken() - .then(function(data) { - data.should.be.a.sha1(); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - generateRefreshToken: function() { - return Promise.resolve({}); - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - generateRefreshToken: function() { - return {}; - } - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - - handler.generateRefreshToken().should.be.an.instanceOf(Promise); - }); - }); - - describe('getAccessTokenExpiresAt()', function() { - it('should return a date', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe('getRefreshTokenExpiresAt()', function() { - it('should return a refresh token', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - var request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `scope`'); - } - }); - - it('should allow the `scope` to be `undefined`', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - should.not.exist(handler.getScope(request)); - }); - - it('should return the scope', function() { - var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - handler.getScope(request).should.equal('foo'); - }); - }); -}); diff --git a/test/integration/grant-types/authorization-code-grant-type.spec.ts b/test/integration/grant-types/authorization-code-grant-type.spec.ts new file mode 100755 index 000000000..f052a7b34 --- /dev/null +++ b/test/integration/grant-types/authorization-code-grant-type.spec.ts @@ -0,0 +1,1033 @@ +import * as should from 'should'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../../../lib/errors'; +import { AuthorizationCodeGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `AuthorizationCodeGrantType` integration. + */ + +describe('AuthorizationCodeGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new AuthorizationCodeGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getAuthorizationCode()`', () => { + try { + new AuthorizationCodeGrantType({ + accessTokenLifetime: 3600, + model: {}, + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `revokeAuthorizationCode()`', () => { + try { + const model = { + getAuthorizationCode: () => {}, + }; + + new AuthorizationCodeGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `revokeAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + }; + + new AuthorizationCodeGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle(undefined, undefined); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is invalid', () => { + const client: any = {}; + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + }); + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', async () => { + const client: any = { id: 'foobar' }; + const token = {}; + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => { + return true; + }, + saveToken: () => { + return token; + }, + validateScope: () => { + return 'foo'; + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await grantType.handle(request, client); + data.should.equal(token); + } catch (e) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode: () => { + return Promise.resolve({ + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }); + }, + revokeAuthorizationCode: () => { + return true; + }, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => { + return true; + }, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const client: any = { id: 'foobar' }; + // const model = { + // getAuthorizationCode: (code, callback) => { + // callback(undefined, { + // authorizationCode: 12345, + // client: { id: 'foobar' }, + // expiresAt: new Date(new Date().getTime() * 2), + // user: {}, + // }); + // }, + // revokeAuthorizationCode: (code, callback) => { + // callback(undefined, { + // authorizationCode: 12345, + // client: { id: 'foobar' }, + // expiresAt: new Date(new Date().getTime() / 2), + // user: {}, + // }); + // }, + // saveToken: (tokenToSave, client, user, callback) => { + // callback(undefined, tokenToSave); + // }, + // }; + // const grantType = new AuthorizationCodeGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + // const request = new Request({ + // body: { code: 12345 }, + // headers: {}, + // method: "ANY", + // query: {}, + // }); + + // grantType.handle(request, client).should.be.an.instanceOf(Promise); + // }); + }); + + describe('getAuthorizationCode()', () => { + it('should throw an error if the request body does not contain `code`', async () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `code`'); + } + }); + + it('should throw an error if `code` is invalid', async () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code`'); + } + }); + + it('should throw an error if `authorizationCode` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => {}, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + }); + }); + + it('should throw an error if `authorizationCode.client` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => { + return { authorizationCode: 12345 }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + }); + }); + + it('should throw an error if `authorizationCode.expiresAt` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => { + return { authorizationCode: 12345, client: {}, user: {} }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `expiresAt` must be a Date instance', + ); + }); + }); + + it('should throw an error if `authorizationCode.user` is missing', () => { + const client: any = {}; + const model = { + getAuthorizationCode: () => { + return { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(), + }; + }, + revokeAuthorizationCode: () => {}, + saveToken: () => {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `user` object', + ); + }); + }); + + it('should throw an error if the client id does not match', () => { + const client: any = { id: 123 }; + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + expiresAt: new Date(), + client: { id: 456 }, + user: {}, + }; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + }); + }); + + it('should throw an error if the auth code is expired', () => { + const client: any = { id: 123 }; + const date = new Date(new Date().getTime() / 2); + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + client: { id: 123 }, + expiresAt: date, + user: {}, + }; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code has expired', + ); + }); + }); + + it('should throw an error if the `redirectUri` is invalid', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + redirectUri: 'foobar', + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return authorizationCode; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: `redirect_uri` is not a valid URI', + ); + }); + }); + + it('should return an auth code', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return authorizationCode; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getAuthorizationCode(request, client) + .then(data => { + data.should.equal(authorizationCode); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return Promise.resolve(authorizationCode); + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client: any = { id: 'foobar' }; + const model = { + getAuthorizationCode() { + return authorizationCode; + }, + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const authorizationCode = { + // authorizationCode: 12345, + // client: { id: 'foobar' }, + // expiresAt: new Date(new Date().getTime() * 2), + // user: {}, + // }; + // const client: any = { id: 'foobar' }; + // const model = { + // getAuthorizationCode(code, callback) { + // callback(undefined, authorizationCode); + // }, + // revokeAuthorizationCode() {}, + // saveToken() {}, + // }; + // const grantType = new AuthorizationCodeGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + // const request = new Request({ + // body: { code: 12345 }, + // headers: {}, + // method: "ANY", + // query: {}, + // }); + + // grantType + // .getAuthorizationCode(request, client) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('validateRedirectUri()', () => { + it('should throw an error if `redirectUri` is missing', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return authorizationCode; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should throw an error if `redirectUri` is invalid', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return true; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { code: 12345, redirect_uri: 'http://bar.foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: `redirect_uri` is invalid'); + } + }); + }); + + describe('revokeAuthorizationCode()', () => { + it('should revoke the auth code', async () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return true; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.revokeAuthorizationCode(authorizationCode); + data.should.equal(authorizationCode); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should throw an error when the auth code is invalid', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return false; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + return grantType + .revokeAuthorizationCode(authorizationCode) + .then(data => { + data.should.equal(authorizationCode); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + }); + }); + + it('should support promises', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return Promise.resolve(true); + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const authorizationCode: any = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() { + return authorizationCode; + }, + saveToken() {}, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const authorizationCode = { + // authorizationCode: 12345, + // client: {}, + // expiresAt: new Date(new Date().getTime() / 2), + // user: {}, + // }; + // const model = { + // getAuthorizationCode() {}, + // revokeAuthorizationCode(code, callback) { + // callback(undefined, authorizationCode); + // }, + // saveToken() {}, + // }; + // const grantType = new AuthorizationCodeGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + + // grantType + // .revokeAuthorizationCode(authorizationCode) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token, ''); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token, '') + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token: any = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken() { + return token; + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token, '') + .should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const token = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken(tokenToSave, client, user, callback) { + callback(undefined, token); + }, + }; + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken({}, {}, token, '').should.be.an.instanceOf(Promise); + }); */ + }); +}); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js deleted file mode 100644 index 7f84e3443..000000000 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ /dev/null @@ -1,594 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var ServerError = require('../../../lib/errors/server-error'); -var should = require('should'); - -/** - * Test `AuthorizationCodeGrantType` integration. - */ - -describe('AuthorizationCodeGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new AuthorizationCodeGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getAuthorizationCode()`', function() { - try { - new AuthorizationCodeGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function() { - try { - var model = { - getAuthorizationCode: function() {} - }; - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {} - }; - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is invalid', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); - }); - - it('should throw an error if `client` is missing', function() { - - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request, null); - } - catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return Promise.resolve({ authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function(code, callback) { callback(null, { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, - revokeAuthorizationCode: function(code, callback) { callback(null, { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }); }, - saveToken: function(tokenToSave, client, user, callback) { callback(null, tokenToSave); } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getAuthorizationCode()', function() { - it('should throw an error if the request body does not contain `code`', function() { - var client = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.getAuthorizationCode(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `code`'); - } - }); - - it('should throw an error if `code` is invalid', function() { - var client = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getAuthorizationCode(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code`'); - } - }); - - it('should throw an error if `authorizationCode` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); - }); - - it('should throw an error if `authorizationCode.client` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345 }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - }); - }); - - it('should throw an error if `authorizationCode.expiresAt` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, user: {} }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `expiresAt` must be a Date instance'); - }); - }); - - it('should throw an error if `authorizationCode.user` is missing', function() { - var client = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); - }); - }); - - it('should throw an error if the client id does not match', function() { - var client = { id: 123 }; - var model = { - getAuthorizationCode: function() { - return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; - }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); - }); - - it('should throw an error if the auth code is expired', function() { - var client = { id: 123 }; - var date = new Date(new Date() / 2); - var model = { - getAuthorizationCode: function() { - return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; - }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code has expired'); - }); - }); - - it('should throw an error if the `redirectUri` is invalid', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); - }); - }); - - it('should return an auth code', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getAuthorizationCode(request, client) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return Promise.resolve(authorizationCode); }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - var client = { id: 'foobar' }; - var model = { - getAuthorizationCode: function(code, callback) { callback(null, authorizationCode); }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('validateRedirectUri()', function() { - it('should throw an error if `redirectUri` is missing', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - } - }); - - it('should throw an error if `redirectUri` is invalid', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { code: 12345, redirect_uri: 'http://bar.foo' }, headers: {}, method: {}, query: {} }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is invalid'); - } - }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should revoke the auth code', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it('should throw an error when the auth code is invalid', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return false; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeAuthorizationCode(authorizationCode) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - }); - }); - - it('should support promises', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return Promise.resolve(true); }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function(code, callback) { callback(null, authorizationCode); }, - saveToken: function() {} - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/grant-types/client-credentials-grant-type.spec.ts b/test/integration/grant-types/client-credentials-grant-type.spec.ts new file mode 100755 index 000000000..d9f1e3a9a --- /dev/null +++ b/test/integration/grant-types/client-credentials-grant-type.spec.ts @@ -0,0 +1,382 @@ +import * as should from 'should'; +import { InvalidArgumentError, InvalidGrantError } from '../../../lib/errors'; +import { ClientCredentialsGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `ClientCredentialsGrantType` integration. + */ + +describe('ClientCredentialsGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new ClientCredentialsGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getUserFromClient()`', () => { + try { + new ClientCredentialsGrantType({ + accessTokenLifetime: 3600, + model: {}, + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getUserFromClient()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getUserFromClient() {}, + }; + + new ClientCredentialsGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getUserFromClient() {}, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getUserFromClient() {}, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const token = {}; + const model = { + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, {} as any) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const token = {}; + const model = { + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, {} as any).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token = {}; + const model = { + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, {} as any).should.be.an.instanceOf(Promise); + }); + }); + + describe('getUserFromClient()', () => { + it('should throw an error if `user` is missing', () => { + const model = { + getUserFromClient() {}, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getUserFromClient({} as any) + .then(() => { + should.fail('should.fail', ''); + }) + .catch((e: any) => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + }); + }); + + it('should return a user', async () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient() { + return user; + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await grantType.getUserFromClient({} as any); + data.should.equal(user); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient() { + return Promise.resolve(user); + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUserFromClient({} as any).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient() { + return user; + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUserFromClient({} as any).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUserFromClient(userId, callback) { + callback(null, user); + }, + saveToken() {}, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUserFromClient({}).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getUserFromClient() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getUserFromClient() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token) + .should.be.an.instanceOf(Promise); + }); + + /* it('should support non-promises', () => { + const token = {}; + const model = { + getUserFromClient() {}, + saveToken() { + return token; + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const token = {}; + const model = { + getUserFromClient() {}, + saveToken(tokenToSave, client, user, callback) { + callback(null, token); + }, + }; + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); */ + }); +}); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js deleted file mode 100644 index 15ec9cc0e..000000000 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ /dev/null @@ -1,256 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var should = require('should'); - -/** - * Test `ClientCredentialsGrantType` integration. - */ - -describe('ClientCredentialsGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new ClientCredentialsGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getUserFromClient()`', function() { - try { - new ClientCredentialsGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getUserFromClient()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getUserFromClient: function() {} - }; - - new ClientCredentialsGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getUserFromClient: function() {}, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getUserFromClient: function() {}, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var token = {}; - var model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, {}) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe('getUserFromClient()', function() { - it('should throw an error if `user` is missing', function() { - var model = { - getUserFromClient: function() {}, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.getUserFromClient(request, {}) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - it('should return a user', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function() { return user; }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.getUserFromClient(request, {}) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function() { return Promise.resolve(user); }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function() {return user; }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUserFromClient: function(userId, callback) { callback(null, user); }, - saveToken: function() {} - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function() { return token; } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getUserFromClient: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/grant-types/implicit-grant-type.spec.ts b/test/integration/grant-types/implicit-grant-type.spec.ts new file mode 100644 index 000000000..0af699fb1 --- /dev/null +++ b/test/integration/grant-types/implicit-grant-type.spec.ts @@ -0,0 +1,269 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { ImplicitGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `ImplicitGrantType` integration. + */ + +describe('ImplicitGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new ImplicitGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = {}; + + new ImplicitGrantType({ model, accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + + it('should throw an error if the `user` parameter is missing', () => { + try { + const model = { + saveToken() {}, + }; + + new ImplicitGrantType({ model, accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `user`'); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + try { + await grantType.handle(); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const client = { id: 'foobar' }; + const token = { accessToken: 'foobar-token' }; + const model = { + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(should.fail); + }); + + it('should support promises', () => { + const client = { id: 'foobar' }; + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client = { id: 'foobar' }; + const model = { + saveToken() {}, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const client = { id: 'foobar' }; + // const model = { + // saveToken(tokenToSave, client, user, callback) { + // callback(null, tokenToSave); + // }, + // }; + // const grantType:any = new ImplicitGrantType({ + // accessTokenLifetime: 123, + // model, + // user: {}, + // }); + // const request = new Request({ + // body: { code: 12345 }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // grantType.handle(request, client).should.be.an.instanceOf(Promise); + // grantType.handle(request, client).then(data => { + // data.should.have.keys('accessToken', 'accessTokenExpiresAt'); + // data.accessToken.should.be.type('string'); + // }); + // }); + }); + + describe('saveToken()', () => { + it('should save the token', () => { + const token = {}; + const model = { + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + return grantType + .saveToken(token) + .then(data => { + data.should.equal(token); + }) + .catch(should.fail); + }); + + it('should support promises', () => { + const token = {}; + const model = { + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token = {}; + const model = { + saveToken() { + return token; + }, + }; + const grantType: any = new ImplicitGrantType({ + accessTokenLifetime: 123, + model, + user: {}, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const token = {}; + // const model = { + // saveToken(tokenToSave, client, user, callback) { + // callback(null, token); + // }, + // }; + // const grantType:any = new ImplicitGrantType({ + // accessTokenLifetime: 123, + // model, + // user: {}, + // }); + + // grantType.saveToken(token).should.be.an.instanceOf(Promise); + // }); + }); +}); diff --git a/test/integration/grant-types/implicit-grant-type_test.js b/test/integration/grant-types/implicit-grant-type_test.js deleted file mode 100644 index 2498cc591..000000000 --- a/test/integration/grant-types/implicit-grant-type_test.js +++ /dev/null @@ -1,260 +0,0 @@ -'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/grant-types/password-grant-type.spec.ts b/test/integration/grant-types/password-grant-type.spec.ts new file mode 100755 index 000000000..a02b37729 --- /dev/null +++ b/test/integration/grant-types/password-grant-type.spec.ts @@ -0,0 +1,506 @@ +import * as should from 'should'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, +} from '../../../lib/errors'; +import { PasswordGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `PasswordGrantType` integration. + */ + +describe('PasswordGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new PasswordGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getUser()`', () => { + try { + new PasswordGrantType({ accessTokenLifetime: 3600, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getUser()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getUser: () => {}, + }; + + new PasswordGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getUser: () => {}, + saveToken: () => {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getUser: () => {}, + saveToken: () => {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle({}, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser: () => { + return {}; + }, + saveToken: () => { + return token; + }, + validateScope: () => { + return 'baz'; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar', scope: 'baz' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser() { + return {}; + }, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser() { + return {}; + }, + saveToken() { + return token; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const client = { id: 'foobar' }; + const token = {}; + const model = { + getUser(username, password, callback) { + callback(null, {}); + }, + saveToken(tokenToSave, client, user, callback) { + callback(null, token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('getUser()', () => { + it('should throw an error if the request body does not contain `username`', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `username`'); + } + }); + + it('should throw an error if the request body does not contain `password`', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `password`'); + } + }); + + it('should throw an error if `username` is invalid', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: '\r\n', password: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `username`'); + } + }); + + it('should throw an error if `password` is invalid', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foobar', password: '\r\n' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getUser(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `password`'); + } + }); + + it('should throw an error if `user` is missing', async () => { + const model = { + getUser() {}, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await grantType.getUser(request); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + } + }); + + it('should return a user', async () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser() { + return user; + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await grantType.getUser(request); + data.should.equal(user); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser() { + return Promise.resolve(user); + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUser(request).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser() { + return user; + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUser(request).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const user = { email: 'foo@bar.com' }; + const model = { + getUser(username, password, callback) { + callback(null, user); + }, + saveToken() {}, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.getUser(request).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getUser() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getUser() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token) + .should.be.an.instanceOf(Promise); + }); + + /* it('should support non-promises', () => { + const token = {}; + const model = { + getUser() {}, + saveToken() { + return token; + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const token = {}; + const model = { + getUser() {}, + saveToken(tokenToSave, client, user, callback) { + callback(null, token); + }, + }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken({}, {}, token).should.be.an.instanceOf(Promise); + }); */ + }); +}); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js deleted file mode 100644 index 9cdf50dd5..000000000 --- a/test/integration/grant-types/password-grant-type_test.js +++ /dev/null @@ -1,344 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var should = require('should'); - -/** - * Test `PasswordGrantType` integration. - */ - -describe('PasswordGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new PasswordGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getUser()`', function() { - try { - new PasswordGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getUser()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getUser: function() {} - }; - - new PasswordGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - try { - grantType.handle({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function() { return {}; }, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function() { return {}; }, - saveToken: function() { return token; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 'foobar' }; - var token = {}; - var model = { - getUser: function(username, password, callback) { callback(null, {}); }, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getUser()', function() { - it('should throw an error if the request body does not contain `username`', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `username`'); - } - }); - - it('should throw an error if the request body does not contain `password`', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `password`'); - } - }); - - it('should throw an error if `username` is invalid', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `username`'); - } - }); - - it('should throw an error if `password` is invalid', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getUser(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `password`'); - } - }); - - it('should throw an error if `user` is missing', function() { - var model = { - getUser: function() {}, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - it('should return a user', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function() { return user; }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return grantType.getUser(request) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function() { return Promise.resolve(user); }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function() { return user; }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var user = { email: 'foo@bar.com' }; - var model = { - getUser: function(username, password, callback) { callback(null, user); }, - saveToken: function() {} - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function() { return token; } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getUser: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/grant-types/refresh-token-grant-type.spec.ts b/test/integration/grant-types/refresh-token-grant-type.spec.ts new file mode 100755 index 000000000..c37ecb9e0 --- /dev/null +++ b/test/integration/grant-types/refresh-token-grant-type.spec.ts @@ -0,0 +1,864 @@ +import * as should from 'should'; +import { + InvalidArgumentError, + InvalidGrantError, + InvalidRequestError, + ServerError, +} from '../../../lib/errors'; +import { RefreshTokenGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `RefreshTokenGrantType` integration. + */ + +describe('RefreshTokenGrantType integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new RefreshTokenGrantType({ accessTokenLifetime: 3600 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getRefreshToken()`', () => { + try { + new RefreshTokenGrantType({ accessTokenLifetime: 3600, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + }); + + it('should throw an error if the model does not implement `revokeToken()`', () => { + try { + const model = { + getRefreshToken() {}, + }; + + new RefreshTokenGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', () => { + try { + const model = { + getRefreshToken() {}, + revokeToken() {}, + }; + + new RefreshTokenGrantType({ accessTokenLifetime: 3600, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getRefreshToken: () => {}, + revokeToken: () => {}, + saveToken: () => {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async () => { + const model = { + getRefreshToken: () => {}, + revokeToken: () => {}, + saveToken: () => {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken: () => { + return token; + }, + revokeToken: () => { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + }, + saveToken: () => { + return token; + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .handle(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return Promise.resolve({ + accessToken: 'foo', + client: { id: 123 }, + user: {}, + }); + }, + revokeToken() { + return Promise.resolve({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }); + }, + saveToken() { + return Promise.resolve({ accessToken: 'foo', client: {}, user: {} }); + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + /* it('should support non-promises', () => { + const client : any= { id: 123 }; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: { id: 123 }, user: {} }; + }, + revokeToken() { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + saveToken() { + return { accessToken: 'foo', client: {}, user: {} }; + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const client : any= { id: 123 }; + const model = { + getRefreshToken(refreshToken, callback) { + callback(null, { accessToken: 'foo', client: { id: 123 }, user: {} }); + }, + revokeToken(refreshToken, callback) { + callback(null, { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }); + }, + saveToken(tokenToSave, client, user, callback) { + callback(null, { accessToken: 'foo', client: {}, user: {} }); + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('getRefreshToken()', () => { + it('should throw an error if the `refreshToken` parameter is missing from the request body', async () => { + const client: any = {}; + const model = { + getRefreshToken: () => {}, + revokeToken: () => {}, + saveToken: () => {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `refresh_token`'); + } + }); + + it('should throw an error if `refreshToken` is not found', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: '12345' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should throw an error if `refreshToken.client` is missing', () => { + const client: any = {}; + const model = { + getRefreshToken() { + return {}; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + }); + }); + + it('should throw an error if `refreshToken.user` is missing', () => { + const client: any = {}; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + }); + }); + + it('should throw an error if the client id does not match', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should throw an error if `refresh_token` contains invalid characters', async () => { + const client: any = {}; + const model = { + getRefreshToken() { + return { client: { id: 456 }, user: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `refresh_token`'); + } + }); + + it('should throw an error if `refresh_token` is missing', () => { + const client: any = {}; + const model = { + getRefreshToken() { + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should throw an error if `refresh_token` is expired', () => { + const client: any = { id: 123 }; + const date = new Date(new Date().getTime() / 2); + const model = { + getRefreshToken() { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: date, + user: {}, + }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token has expired'); + }); + }); + + it('should throw an error if `refreshTokenExpiresAt` is not a date value', () => { + const client: any = { id: 123 }; + const model = { + getRefreshToken() { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: 'stringValue', + user: {}, + }; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + }); + }); + + it('should return a token', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken() { + return token; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return grantType + .getRefreshToken(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken() { + return Promise.resolve(token); + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const client: any = { id: 123 }; + const token: any = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = { + getRefreshToken() { + return token; + }, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const client : any= { id: 123 }; + // const token : any= { accessToken: 'foo', client: { id: 123 }, user: {} }; + // const model = { + // getRefreshToken(refreshToken, callback) { + // callback(undefined, token); + // }, + // revokeToken() {}, + // saveToken() {}, + // }; + // const grantType = new RefreshTokenGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + // const request = new Request({ + // body: { refresh_token: 'foobar' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // grantType + // .getRefreshToken(request, client) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('revokeToken()', () => { + it('should throw an error if the `token` is invalid', () => { + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + grantType + .revokeToken({} as any) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + }); + }); + + it('should revoke the token', () => { + const token: any = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken() { + return token; + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + return grantType + .revokeToken(token) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const token: any = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken() { + return Promise.resolve(token); + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const token: any = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken() { + return token; + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const token : any= { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + const model = { + getRefreshToken() {}, + revokeToken(refreshToken, callback) { + callback(undefined, token); + }, + saveToken() {}, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('saveToken()', () => { + it('should save the token', async () => { + const token: any = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken() { + return token; + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + try { + const data = await grantType.saveToken({}, {} as any, token); + data.should.equal(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should support promises', () => { + const token: any = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken() { + return Promise.resolve(token); + }, + }; + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType + .saveToken({}, {} as any, token) + .should.be.an.instanceOf(Promise); + }); + + // it('should support non-promises', () => { + // const token : any= {}; + // const model = { + // getRefreshToken() {}, + // revokeToken() {}, + // saveToken() { + // return token; + // }, + // }; + // const grantType = new RefreshTokenGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + + // grantType.saveToken(token).should.be.an.instanceOf(Promise); + // }); + + // it('should support callbacks', () => { + // const token : any= {}; + // const model = { + // getRefreshToken() {}, + // revokeToken() {}, + // saveToken(tokenToSave, client, user, callback) { + // callback(null, token); + // }, + // }; + // const grantType = new RefreshTokenGrantType({ + // accessTokenLifetime: 123, + // model, + // }); + + // grantType.saveToken(token).should.be.an.instanceOf(Promise); + // }); + }); +}); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js deleted file mode 100644 index 925396afe..000000000 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ /dev/null @@ -1,536 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var Promise = require('bluebird'); -var RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); -var Request = require('../../../lib/request'); -var ServerError = require('../../../lib/errors/server-error'); -var should = require('should'); - -/** - * Test `RefreshTokenGrantType` integration. - */ - -describe('RefreshTokenGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new RefreshTokenGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getRefreshToken()`', function() { - try { - new RefreshTokenGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getRefreshToken()`'); - } - }); - - it('should throw an error if the model does not implement `revokeToken()`', function() { - try { - var model = { - getRefreshToken: function() {} - }; - - new RefreshTokenGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeToken()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {} - }; - - new RefreshTokenGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - try { - grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return token; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { return Promise.resolve({ accessToken: 'foo', client: { id: 123 }, user: {} }); }, - revokeToken: function() { return Promise.resolve({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }); }, - saveToken: function() { return Promise.resolve({ accessToken: 'foo', client: {}, user: {} }); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: function() { return { accessToken: 'foo', client: {}, user: {} }; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function(refreshToken, callback) { callback(null, { accessToken: 'foo', client: { id: 123 }, user: {} }); }, - revokeToken: function(refreshToken, callback) { callback(null, { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }); }, - saveToken: function(tokenToSave, client, user, callback) { callback(null,{ accessToken: 'foo', client: {}, user: {} }); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getRefreshToken()', function() { - it('should throw an error if the `refreshToken` parameter is missing from the request body', function() { - var client = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `refresh_token`'); - } - }); - - it('should throw an error if `refreshToken` is not found', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { return; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should throw an error if `refreshToken.client` is missing', function() { - var client = {}; - var model = { - getRefreshToken: function() { return {}; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); - }); - }); - - it('should throw an error if `refreshToken.user` is missing', function() { - var client = {}; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); - }); - }); - - it('should throw an error if the client id does not match', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 456 }, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should throw an error if `refresh_token` contains invalid characters', function() { - var client = {}; - var model = { - getRefreshToken: function() { - return { client: { id: 456 }, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `refresh_token`'); - } - }); - - it('should throw an error if `refresh_token` is missing', function() { - var client = {}; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 456 }, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should throw an error if `refresh_token` is expired', function() { - var client = { id: 123 }; - var date = new Date(new Date() / 2); - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token has expired'); - }); - }); - - it('should throw an error if `refreshTokenExpiresAt` is not a date value', function() { - var client = { id: 123 }; - var model = { - getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', user: {} }; - }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); - }); - }); - - it('should return a token', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - return grantType.getRefreshToken(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return Promise.resolve(token); }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var client = { id: 123 }; - var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - var model = { - getRefreshToken: function(refreshToken, callback) { callback(null, token); }, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('revokeToken()', function() { - it('should throw an error if the `token` is invalid', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - grantType.revokeToken({}) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - }); - }); - - it('should revoke the token', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function() { return token; }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.revokeToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function() { return Promise.resolve(token); }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function() { return token; }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - var model = { - getRefreshToken: function() {}, - revokeToken: function(refreshToken, callback) { callback(null, token); }, - saveToken: function() {} - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return token; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - return grantType.saveToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return Promise.resolve(token); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function() { return token; } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var token = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: function(tokenToSave, client, user, callback) { callback(null, token); } - }; - var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); -}); diff --git a/test/integration/handlers/authenticate-handler.spec.ts b/test/integration/handlers/authenticate-handler.spec.ts new file mode 100755 index 000000000..9200e9d01 --- /dev/null +++ b/test/integration/handlers/authenticate-handler.spec.ts @@ -0,0 +1,710 @@ +import * as should from 'should'; +import { + AccessDeniedError, + InsufficientScopeError, + InvalidArgumentError, + InvalidRequestError, + InvalidTokenError, + ServerError, + UnauthorizedRequestError, +} from '../../../lib/errors'; +import { AuthenticateHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; + +/** + * Test `AuthenticateHandler` integration. + */ + +describe('AuthenticateHandler integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.model` is missing', () => { + try { + new AuthenticateHandler(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getAccessToken()`', () => { + try { + new AuthenticateHandler({ model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + }); + + it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', () => { + try { + new AuthenticateHandler({ + model: { getAccessToken() {} }, + scope: 'foobar', + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `addAcceptedScopesHeader`'); + } + }); + + it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', () => { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + model: { getAccessToken() {} }, + scope: 'foobar', + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Missing parameter: `addAuthorizedScopesHeader`', + ); + } + }); + + it('should throw an error if `scope` was given and the model does not implement `verifyScope()`', () => { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: { getAccessToken() {} }, + scope: 'foobar', + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `verifyScope()`', + ); + } + }); + + it('should set the `model`', () => { + const model = { getAccessToken() {} }; + const grantType = new AuthenticateHandler({ model }); + + grantType.model.should.equal(model); + }); + + it('should set the `scope`', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const grantType = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foobar', + }); + + grantType.scope.should.equal('foobar'); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + try { + await handler.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', () => { + const model = { + getAccessToken() { + throw new UnauthorizedRequestError(undefined, undefined); + }, + }; + const handler = new AuthenticateHandler({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('WWW-Authenticate') + .should.equal('Bearer realm="Service"'); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getAccessToken() { + throw new AccessDeniedError('Cannot request this access token'); + }, + }; + const handler = new AuthenticateHandler({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Cannot request this access token'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', () => { + const model = { + getAccessToken() { + throw new Error('Unhandled exception'); + }, + }; + const handler = new AuthenticateHandler({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + }); + }); + + it('should return an access token', () => { + const accessToken: any = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = { + getAccessToken() { + return accessToken; + }, + verifyScope() { + return true; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(data => { + data.should.equal(accessToken); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('getTokenFromRequest()', () => { + it('should throw an error if more than one authentication method is used', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { access_token: 'foo' }, + }); + + try { + handler.getTokenFromRequest(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: only one authentication method is allowed', + ); + } + }); + + it('should throw an error if `accessToken` is missing', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequest(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedRequestError); + e.message.should.equal('Unauthorized request: no authentication given'); + } + }); + }); + + describe('getTokenFromRequestHeader()', () => { + it('should throw an error if the token is malformed', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'foobar', + }, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequestHeader(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: malformed authorization header', + ); + } + }); + + it('should return the bearer token', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: {}, + }); + + const bearerToken = handler.getTokenFromRequestHeader(request); + + bearerToken.should.equal('foo'); + }); + }); + + describe('getTokenFromRequestQuery()', () => { + it('should throw an error if the query contains a token', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + try { + handler.getTokenFromRequestQuery(undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: do not send bearer tokens in query URLs', + ); + } + }); + + it('should return the bearer token if `allowBearerTokensInQueryString` is true', () => { + const handler = new AuthenticateHandler({ + allowBearerTokensInQueryString: true, + model: { getAccessToken() {} }, + }); + const req = { query: { access_token: 'foo' } }; + handler.getTokenFromRequestQuery(req as Request).should.equal('foo'); + }); + }); + + describe('getTokenFromRequestBody()', () => { + it('should throw an error if the method is `GET`', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'GET', + query: {}, + }); + + try { + handler.getTokenFromRequestBody(request); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: token may not be passed in the body when using the GET verb', + ); + } + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequestBody(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + }); + + it('should return the bearer token', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + handler.getTokenFromRequestBody(request).should.equal('foo'); + }); + }); + + describe('getAccessToken()', () => { + it('should throw an error if `accessToken` is missing', () => { + const model = { + getAccessToken() {}, + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token is invalid'); + }); + }); + + it('should throw an error if `accessToken.user` is missing', () => { + const model = { + getAccessToken() { + return {}; + }, + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + }); + }); + + it('should return an access token', () => { + const accessToken: any = { user: {} }; + const model = { + getAccessToken() { + return accessToken; + }, + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(data => { + data.should.equal(accessToken); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const model = { + getAccessToken() { + return Promise.resolve({ user: {} }); + }, + }; + const handler = new AuthenticateHandler({ model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + getAccessToken() { + return { user: {} }; + }, + }; + const handler = new AuthenticateHandler({ model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const model = { + getAccessToken(token, callback) { + callback(null, { user: {} }); + }, + }; + const handler = new AuthenticateHandler({ model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); */ + }); + + describe('validateAccessToken()', () => { + it('should throw an error if `accessToken` is expired', () => { + const accessToken: any = { + accessTokenExpiresAt: new Date(new Date().getTime() / 2), + }; + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + try { + handler.validateAccessToken(accessToken); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token has expired'); + } + }); + + it('should return an access token', () => { + const accessToken: any = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + + handler.validateAccessToken(accessToken).should.equal(accessToken); + }); + }); + + describe('verifyScope()', () => { + it('should throw an error if `scope` is insufficient', () => { + const model = { + getAccessToken() {}, + verifyScope() { + return false; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + return handler + .verifyScope('foo' as any) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InsufficientScopeError); + e.message.should.equal( + 'Insufficient scope: authorized scope is insufficient', + ); + }); + }); + + it('should support promises', () => { + const model = { + getAccessToken() {}, + verifyScope() { + return true; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + handler.verifyScope('foo' as any).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + getAccessToken() {}, + verifyScope() { + return true; + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + handler.verifyScope('foo' as any).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const model = { + getAccessToken() {}, + verifyScope(token, scope, callback) { + callback(null, true); + }, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'foo', + }); + + handler.verifyScope('foo').should.be.an.instanceOf(Promise); + }); */ + }); + + describe('updateResponse()', () => { + it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.headers.should.not.have.property('x-accepted-oauth-scopes'); + }); + + it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model, + scope: 'foo bar', + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); + }); + + it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.headers.should.not.have.property('x-oauth-scopes'); + }); + + it('should set the `X-Authorized-OAuth-Scopes` header', () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model, + scope: 'foo bar', + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: 'foo biz' } as any); + + response.get('X-OAuth-Scopes').should.equal('foo biz'); + }); + }); +}); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js deleted file mode 100644 index 6ed227eaa..000000000 --- a/test/integration/handlers/authenticate-handler_test.js +++ /dev/null @@ -1,564 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AccessDeniedError = require('../../../lib/errors/access-denied-error'); -var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var InsufficientScopeError = require('../../../lib/errors/insufficient-scope-error'); -var InvalidTokenError = require('../../../lib/errors/invalid-token-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var ServerError = require('../../../lib/errors/server-error'); -var UnauthorizedRequestError = require('../../../lib/errors/unauthorized-request-error'); -var should = require('should'); - -/** - * Test `AuthenticateHandler` integration. - */ - -describe('AuthenticateHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.model` is missing', function() { - try { - new AuthenticateHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getAccessToken()`', function() { - try { - new AuthenticateHandler({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); - } - }); - - it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', function() { - try { - new AuthenticateHandler({ model: { getAccessToken: function() {} }, scope: 'foobar' }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `addAcceptedScopesHeader`'); - } - }); - - it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', function() { - try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, model: { getAccessToken: function() {} }, scope: 'foobar' }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `addAuthorizedScopesHeader`'); - } - }); - - it('should throw an error if `scope` was given and the model does not implement `verifyScope()`', function() { - try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: { getAccessToken: function() {} }, scope: 'foobar' }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `verifyScope()`'); - } - }); - - it('should set the `model`', function() { - var model = { getAccessToken: function() {} }; - var grantType = new AuthenticateHandler({ model: model }); - - grantType.model.should.equal(model); - }); - - it('should set the `scope`', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var grantType = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: 'foobar' - }); - - grantType.scope.should.equal('foobar'); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', function() { - var model = { - getAccessToken: function() { - throw new UnauthorizedRequestError(); - } - }; - var handler = new AuthenticateHandler({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('WWW-Authenticate').should.equal('Bearer realm="Service"'); - }); - }); - - it('should throw the error if an oauth error is thrown', function() { - var model = { - getAccessToken: function() { - throw new AccessDeniedError('Cannot request this access token'); - } - }; - var handler = new AuthenticateHandler({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Cannot request this access token'); - }); - }); - - it('should throw a server error if a non-oauth error is thrown', function() { - var model = { - getAccessToken: function() { - throw new Error('Unhandled exception'); - } - }; - var handler = new AuthenticateHandler({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Unhandled exception'); - }); - }); - - it('should return an access token with extend model obj with request', function() { - var accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - var model = { - getAccessToken: function() { - return accessToken; - }, - verifyScope: function() { - return true; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - var request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - model.request.should.equal(request); - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - }); - - describe('getTokenFromRequest()', function() { - it('should throw an error if more than one authentication method is used', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: { access_token: 'foo' } - }); - - try { - handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: only one authentication method is allowed'); - } - }); - - it('should throw an error if `accessToken` is missing', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedRequestError); - e.message.should.equal('Unauthorized request: no authentication given'); - } - }); - }); - - describe('getTokenFromRequestHeader()', function() { - it('should throw an error if the token is malformed', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { - 'Authorization': 'foobar' - }, - method: {}, - query: {} - }); - - try { - handler.getTokenFromRequestHeader(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: malformed authorization header'); - } - }); - - it('should return the bearer token', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: {} - }); - - var bearerToken = handler.getTokenFromRequestHeader(request); - - bearerToken.should.equal('foo'); - }); - }); - - describe('getTokenFromRequestQuery()', function() { - it('should throw an error if the query contains a token', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - handler.getTokenFromRequestQuery(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: do not send bearer tokens in query URLs'); - } - }); - - it('should return the bearer token if `allowBearerTokensInQueryString` is true', function() { - var handler = new AuthenticateHandler({ allowBearerTokensInQueryString: true, model: { getAccessToken: function() {} } }); - - handler.getTokenFromRequestQuery({ query: { access_token: 'foo' } }).should.equal('foo'); - }); - }); - - describe('getTokenFromRequestBody()', function() { - it('should throw an error if the method is `GET`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: 'GET', - query: {} - }); - - try { - handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: token may not be passed in the body when using the GET verb'); - } - }); - - it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: {}, - query: {} - }); - - try { - handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); - } - }); - - it('should return the bearer token', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: {}, - query: {} - }); - - handler.getTokenFromRequestBody(request).should.equal('foo'); - }); - }); - - describe('getAccessToken()', function() { - it('should throw an error if `accessToken` is missing', function() { - var model = { - getAccessToken: function() {} - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token is invalid'); - }); - }); - - it('should throw an error if `accessToken.user` is missing', function() { - var model = { - getAccessToken: function() { - return {}; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAccessToken()` did not return a `user` object'); - }); - }); - - it('should return an access token', function() { - var accessToken = { user: {} }; - var model = { - getAccessToken: function() { - return accessToken; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - getAccessToken: function() { - return Promise.resolve({ user: {} }); - } - }; - var handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getAccessToken: function() { - return { user: {} }; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getAccessToken: function(token, callback) { - callback(null, { user: {} }); - } - }; - var handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - }); - - describe('validateAccessToken()', function() { - it('should throw an error if `accessToken` is expired', function() { - var accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - handler.validateAccessToken(accessToken); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token has expired'); - } - }); - - it('should return an access token', function() { - var accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - handler.validateAccessToken(accessToken).should.equal(accessToken); - }); - }); - - describe('verifyScope()', function() { - it('should throw an error if `scope` is insufficient', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() { - return false; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - return handler.verifyScope('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal('Insufficient scope: authorized scope is insufficient'); - }); - }); - - it('should support promises', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope('foo').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope('foo').should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function(token, scope, callback) { - callback(null, true); - } - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope('foo').should.be.an.instanceOf(Promise); - }); - }); - - describe('updateResponse()', function() { - it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.headers.should.not.have.property('x-accepted-oauth-scopes'); - }); - - it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: 'foo bar' }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); - }); - - it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.headers.should.not.have.property('x-oauth-scopes'); - }); - - it('should set the `X-Authorized-OAuth-Scopes` header', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: 'foo bar' }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: 'foo biz' }); - - response.get('X-OAuth-Scopes').should.equal('foo biz'); - }); - }); -}); diff --git a/test/integration/handlers/authorize-handler.spec.ts b/test/integration/handlers/authorize-handler.spec.ts new file mode 100755 index 000000000..81eb04700 --- /dev/null +++ b/test/integration/handlers/authorize-handler.spec.ts @@ -0,0 +1,1622 @@ +import * as should from 'should'; +import * as url from 'url'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidScopeError, + ServerError, + UnauthorizedClientError, +} from '../../../lib/errors'; +import { AuthenticateHandler, AuthorizeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; +import { CodeResponseType } from '../../../lib/response-types'; + +/** + * Test `AuthorizeHandler` integration. + */ + +describe('AuthorizeHandler integration', () => { + describe('constructor()', () => { + // Move to Code Response Type + // it('should throw an error if `options.authorizationCodeLifetime` is missing', () => { + // try { + // new AuthorizeHandler({ model: {} }); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(InvalidArgumentError); + // e.message.should.equal( + // 'Missing parameter: `authorizationCodeLifetime`', + // ); + // } + // }); + + it('should throw an error if `options.model` is missing', () => { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', () => { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + // Move to Code Response Type + // it('should throw an error if the model does not implement `saveAuthorizationCode()`', () => { + // try { + // new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model: { getClient: () => {} }, + // }); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(InvalidArgumentError); + // e.message.should.equal( + // 'Invalid argument: model does not implement `saveAuthorizationCode()`', + // ); + // } + // }); + + it('should throw an error if the model does not implement `getAccessToken()`', () => { + const model = { + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + }); + + // it('should set the `authorizationCodeLifetime`', () => { + // const model = { + // getAccessToken: () => {}, + // getClient: () => {}, + // saveAuthorizationCode: () => {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler.authorizationCodeLifetime.should.equal(120); + // }); + + it('should set the `authenticateHandler`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); + }); + + it('should set the `model`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + try { + await handler.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if `allowed` is `false`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { allowed: 'false' }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal( + 'Access denied: user denied access to application', + ); + }); + }); + + it('should redirect to an error response if a non-oauth error is thrown', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new Error('Unhandled exception'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('location') + .should.equal( + 'http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar', + ); + }); + }); + + it('should redirect to an error response if an oauth error is thrown', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new AccessDeniedError('Cannot request this auth code'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.get('location').should.equal( + // tslint:disable-next-line:max-line-length + 'http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar', + ); + }); + }); + + it('should redirect to a successful response with `code` and `state` if successful', () => { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = { + getAccessToken: () => { + return { + client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return client; + }, + saveAuthorizationCode: () => { + return { authorizationCode: 12345, client }; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + response + .get('location') + .should.equal('http://example.com/cb?code=12345&state=foobar'); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should redirect to an error response if `scope` is invalid', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + return {}; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + scope: [], + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60', + ); + }); + }); + + it('should redirect to an error response if `state` is missing', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new AccessDeniedError('Cannot request this auth code'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60', + ); + }); + }); + + it('should redirect to an error response if `response_type` is invalid', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + return { authorizationCode: 12345, client: {} }; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'test', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.get('location').should.equal( + // tslint:disable-next-line:max-line-length + 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar', + ); + }); + }); + + it('should fail on invalid `response_type` before calling model.saveAuthorizationCode()', () => { + const model = { + getAccessToken: () => { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: () => { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => { + throw new Error('must not be reached'); + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'test', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.get('location').should.equal( + // tslint:disable-next-line:max-line-length + 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar', + ); + }); + }); + + it('should return the `code` if successful', () => { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = { + getAccessToken: () => { + return { + client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() { + return client; + }, + saveAuthorizationCode() { + return { authorizationCode: 12345, client }; + }, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: 'ANY', + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(data => { + data.should.eql({ + authorizationCode: 12345, + client, + }); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + // describe('generateAuthorizationCode()', () => { + // it('should return an auth code', async () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // try { + // const data: any = await handler.generateAuthorizationCode( + // undefined, + // undefined, + // undefined, + // ); + // data.should.be.a.sha1(); + // } catch (error) { + // should.fail('should.fail', ''); + // } + // }); + + // it('should support promises', async () => { + // const model = { + // generateAuthorizationCode() { + // return Promise.resolve({}); + // }, + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // try { + // await handler + // .generateAuthorizationCode(undefined, undefined, undefined) + // .should.be.an.instanceOf(Promise); + // } catch (error) { + // should.fail('should.fail', ''); + // } + // }); + + // /* it('should support non-promises', () => { + // const model = { + // generateAuthorizationCode() { + // return {}; + // }, + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .generateAuthorizationCode(undefined, undefined, undefined) + // .should.be.an.instanceOf(Promise); + // }); */ + // }); + + // describe('getAuthorizationCodeLifetime()', () => { + // it('should return a date', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); + // }); + // }); + + describe('getClient()', () => { + it('should throw an error if `client_id` is missing', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `client_id`'); + } + }); + + it('should throw an error if `client_id` is invalid', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 'øå€£‰', response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `client.redirectUri` is invalid', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + redirect_uri: 'foobar', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should throw an error if `client` is missing', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: client credentials are invalid', + ); + }); + }); + + it('should throw an error if `client.grants` is missing', () => { + const model = { + getAccessToken() {}, + getClient() { + return {}; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: missing client `grants`'); + }); + }); + + it('should throw an error if `client` is unauthorized', () => { + const model = { + getAccessToken() {}, + getClient() { + return { grants: [] }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal( + 'Unauthorized client: `grant_type` is invalid', + ); + }); + }); + + it('should throw an error if `client.redirectUri` is missing', () => { + const model = { + getAccessToken() {}, + getClient() { + return { grants: ['authorization_code'] }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: missing client `redirectUri`', + ); + }); + }); + + it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', () => { + const model = { + getAccessToken() {}, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['https://example.com'], + }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + redirect_uri: 'https://foobar.com', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: `redirect_uri` does not match client value', + ); + }); + }); + + it('should support promises', async () => { + const model = { + getAccessToken() {}, + async getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + handler.getClient(request).should.be.an.instanceOf(Promise); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + /* it('should support non-promises', async () => { + const model = { + getAccessToken() {}, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + await handler.getClient(request).should.be.an.instanceOf(Promise); + }); */ + + /* it('should support callbacks', () => { + const model = { + getAccessToken() {}, + getClient(clientId, clientSecret, callback) { + should.equal(clientSecret, null); + callback(null, { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }); + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); */ + + describe('with `client_id` in the request query', () => { + it('should return a client', () => { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = { + getAccessToken() {}, + getClient() { + return client; + }, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: 'ANY', + query: { client_id: 12345 }, + }); + + return handler + .getClient(request) + .then(data => { + data.should.equal(client); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + }); + + describe('getScope()', () => { + it('should throw an error if `scope` is invalid', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getScope(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + } + }); + + describe('with `scope` in the request body', () => { + it('should return the scope', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getScope(request).should.equal('foo'); + }); + }); + + describe('with `scope` in the request query', () => { + it('should return the scope', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { scope: 'foo' }, + }); + + handler.getScope(request).should.equal('foo'); + }); + }); + }); + + describe('getState()', () => { + it('should throw an error if `allowEmptyState` is false and `state` is missing', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + allowEmptyState: false, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getState(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + } + }); + + it('should throw an error if `state` is invalid', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { state: 'øå€£‰' }, + }); + + try { + handler.getState(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `state`'); + } + }); + + describe('with `state` in the request body', () => { + it('should return the state', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { state: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getState(request).should.equal('foobar'); + }); + }); + + describe('with `state` in the request query', () => { + it('should return the state', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { state: 'foobar' }, + }); + + handler.getState(request).should.equal('foobar'); + }); + }); + }); + + describe('getUser()', () => { + it('should throw an error if `user` is missing', () => { + const authenticateHandler = { handle() {} }; + const model = { + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `handle()` did not return a `user` object', + ); + }); + }); + + it('should return a user', () => { + const user = {}; + const model = { + getAccessToken() { + return { + user, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getUser(request, response) + .then(data => { + data.should.equal(user); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + // describe('saveAuthorizationCode()', () => { + // it('should return an auth code', () => { + // const authorizationCode = {}; + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() { + // return authorizationCode; + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // return handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'biz', + // 'baz' as any, + // undefined, + // undefined, + // ) + // .then(data => { + // data.should.equal(authorizationCode); + // }) + // .catch(() => should.fail('should.fail', '')); + // }); + + // it('should support promises when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() { + // return Promise.resolve({}); + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'biz', + // 'baz' as any, + // undefined, + // undefined, + // ) + // .should.be.an.instanceOf(Promise); + // }); + + // /* it('should support non-promises when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() { + // return {}; + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode('foo', 'bar', 'biz', 'baz', undefined, undefined) + // .should.be.an.instanceOf(Promise); + // }); */ + + // /* it('should support callbacks when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode(code, client, user, callback) { + // return callback(null, true); + // }, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + // .should.be.an.instanceOf(Promise); + // }); */ + // }); + + // describe('getResponseType()', () => { + // it('should throw an error if `response_type` is missing', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: {}, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // try { + // handler.getResponseType(request); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(InvalidRequestError); + // e.message.should.equal('Missing parameter: `response_type`'); + // } + // }); + + // it('should throw an error if `response_type` is not `code`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: { response_type: 'foobar' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // try { + // handler.getResponseType(request); + + // should.fail('should.fail', ''); + // } catch (e) { + // e.should.be.an.instanceOf(UnsupportedResponseTypeError); + // e.message.should.equal( + // 'Unsupported response type: `response_type` is not supported', + // ); + // } + // }); + + // describe('with `response_type` in the request body', () => { + // it('should return a response type', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: { response_type: 'code' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + // const ResponseType = handler.getResponseType(request); + + // ResponseType.should.equal(CodeResponseType); + // }); + // }); + + // describe('with `response_type` in the request query', () => { + // it('should return a response type', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // const request = new Request({ + // body: {}, + // headers: {}, + // method: 'ANY', + // query: { response_type: 'code' }, + // }); + // const ResponseType = handler.getResponseType(request); + + // ResponseType.should.equal(CodeResponseType); + // }); + // }); + // }); + + describe('buildSuccessRedirectUri()', () => { + it('should return a redirect uri', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + responseType.code = 12345; + const redirectUri = handler.buildSuccessRedirectUri( + 'http://example.com/cb', + responseType, + ); + + url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); + }); + }); + + describe('buildErrorRedirectUri()', () => { + it('should set `error_description` if available', () => { + const error = new InvalidClientError('foo bar'); + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + const redirectUri = handler.buildErrorRedirectUri( + 'http://example.com/cb', + responseType, + error, + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?error=invalid_client&error_description=foo%20bar', + ); + }); + + it('should return a redirect uri', () => { + const error = new InvalidClientError(); + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + const redirectUri = handler.buildErrorRedirectUri( + 'http://example.com/cb', + responseType, + error, + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?error=invalid_client&error_description=Bad%20Request', + ); + }); + }); + + describe('updateResponse()', () => { + it('should set the `location` header', () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 360, + model: { saveAuthorizationCode: () => {} }, + }); + const response = new Response({ body: {}, headers: {} }); + const uri = url.parse('http://example.com/cb', true); + + handler.updateResponse(response, uri, responseType, 'foobar'); + + response + .get('location') + .should.equal('http://example.com/cb?state=foobar'); + }); + }); +}); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js deleted file mode 100644 index 0cb8faddc..000000000 --- a/test/integration/handlers/authorize-handler_test.js +++ /dev/null @@ -1,1106 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AccessDeniedError = require('../../../lib/errors/access-denied-error'); -var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); -var CodeResponseType = require('../../../lib/response-types/code-response-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidClientError = require('../../../lib/errors/invalid-client-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); -var UnsupportedResponseTypeError = require('../../../lib/errors/unsupported-response-type-error'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var ServerError = require('../../../lib/errors/server-error'); -var UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); -var should = require('should'); -var url = require('url'); - -/** - * Test `AuthorizeHandler` integration. - */ - -describe('AuthorizeHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.model` is missing', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getClient()`', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getClient()`'); - } - }); - - it('should throw an error if the model does not implement `getAccessToken()`', function() { - var model = { - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); - } - }); - - it('should set the `authenticateHandler`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - try { - handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should throw an error if `response` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - }); - - it('should throw an error if `allowed` is `false`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { allowed: 'false' } }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Access denied: user denied access to application'); - }); - }); - - it('should redirect to an error response if a non-oauth error is thrown', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new Error('Unhandled exception'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); - }); - }); - - it('should redirect to an error response if an oauth error is thrown', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new AccessDeniedError('Cannot request this auth code'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); - }); - }); - - it('should redirect to a successful response with `code` and `state` if successful', function() { - var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - var model = { - getAccessToken: function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - response.get('location').should.equal('http://example.com/cb?code=12345&state=foobar'); - }) - .catch(should.fail); - }); - - - it('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() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return {}; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - scope: [], - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60'); - }); - }); - - it('should redirect to 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() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new AccessDeniedError('Cannot request this auth code'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); - }); - }); - - it('should redirect to an error response if `response_type` is invalid', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: {} }; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'test' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); - }); - - it('should fail on invalid `response_type` before calling model.saveAuthorizationCode()', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - throw new Error('must not be reached'); - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'test' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('location').should.equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - }); - }); - - it('should return the `code` if successful with extend model obj with request', function() { - var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - var model = { - getAccessToken: function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 12345, client: client }; - } - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - model.request.should.equal(request); - data.should.eql({ - authorizationCode: 12345, - client: client - }); - }) - .catch(should.fail); - }); - }); - - describe('getClient()', function() { - it('should throw an error if `client_id` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `client_id`'); - } - }); - - it('should throw an error if `client_id` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 'øå€£‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - } - }); - - it('should throw an error if `client.redirectUri` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client credentials are invalid'); - }); - }); - - it('should throw an error if `client.grants` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return {}; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: missing client `grants`'); - }); - }); - - it('should throw an error if `client` is unauthorized', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return { grants: [] }; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - }); - }); - - it('should throw an error if `client.redirectUri` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { return { grants: ['authorization_code'] }; }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: missing client `redirectUri`'); - }); - }); - - it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['https://example.com'] }; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: `redirect_uri` does not match client value'); - }); - }); - - it('should support promises', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return Promise.resolve({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }); - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getAccessToken: function() {}, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getAccessToken: function() {}, - getClient: function(clientId, clientSecret, callback) { - should.equal(clientSecret, null); - callback(null, { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }); - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - describe('with `client_id` in the request query', function() { - it('should return a client', function() { - var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - var model = { - getAccessToken: function() {}, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: { client_id: 12345 } }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid parameter: `scope`'); - } - }); - - describe('with `scope` in the request body', function() { - it('should return the scope', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - handler.getScope(request).should.equal('foo'); - }); - }); - - describe('with `scope` in the request query', function() { - it('should return the scope', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { scope: 'foo' } }); - - handler.getScope(request).should.equal('foo'); - }); - }); - }); - - describe('getState()', function() { - it('should throw an error if `allowEmptyState` is false and `state` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `state`'); - } - }); - - it('should throw an error if `state` is invalid', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'øå€£‰' } }); - - try { - handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `state`'); - } - }); - - describe('with `state` in the request body', function() { - it('should return the state', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); - - handler.getState(request).should.equal('foobar'); - }); - }); - - describe('with `state` in the request query', function() { - it('should return the state', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); - - handler.getState(request).should.equal('foobar'); - }); - }); - }); - - describe('getUser()', function() { - it('should throw an error if `user` is missing', function() { - var authenticateHandler = { handle: function() {} }; - var model = { - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - var response = new Response(); - - return handler.getUser(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `handle()` did not return a `user` object'); - }); - }); - - it('should return a user', function() { - var user = {}; - var model = { - getAccessToken: function() { - return { - user: user, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.getUser(request, response) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - }); - - describe('getResponseType()', function() { - it('should throw an error if `response_type` is missing', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `response_type`'); - } - }); - - it('should throw an error if `response_type` is not `code` or `token`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal('Unsupported response type: `response_type` is not supported'); - } - }); - - describe('with `response_type` in the request body', function() { - it('should return a response type', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - var ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - - describe('with `response_type` in the request query', function() { - it('should return a response type', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); - var ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - }); - - describe('buildSuccessRedirectUri()', function() { - it('should return a redirect uri', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var responseType = new CodeResponseType({ 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'); - }); - }); - - describe('buildErrorRedirectUri()', function() { - it('should set `error_description` if available', function() { - var error = new InvalidClientError('foo bar'); - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var 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'); - }); - - it('should return a redirect uri', function() { - var error = new InvalidClientError(); - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var 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'); - }); - }); - - describe('updateResponse()', function() { - it('should set the `location` header', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var response = new Response({ body: {}, headers: {} }); - var uri = url.parse('http://example.com/cb'); - var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: { - saveAuthorizationCode: function() {} - }}); - - 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.spec.ts b/test/integration/handlers/revoke-handler.spec.ts new file mode 100644 index 000000000..362802b9b --- /dev/null +++ b/test/integration/handlers/revoke-handler.spec.ts @@ -0,0 +1,1114 @@ +import * as should from 'should'; +import * as util from 'util'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + InvalidTokenError, + ServerError, +} from '../../../lib/errors'; +import { RevokeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; + +/** + * Test `RevokeHandler` integration. + */ + +describe('RevokeHandler integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.model` is missing', () => { + try { + new RevokeHandler({}); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', () => { + try { + new RevokeHandler({ model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + it('should set the `model`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + try { + await handler.handle(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handle(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if the method is not `POST`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'GET', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: method must be POST'); + }); + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'hash' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getClient() { + return { grants: ['password'] }; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it('should update the response if an error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(() => { + response.body.should.eql({ + error: 'server_error', + error_description: 'Unhandled exception', + }); + response.status.should.equal(500); + }); + }); + + it('should not update the response if an invalid token error is thrown', () => { + const token = { + refreshToken: 'hash', + client: {}, + user: {}, + refreshTokenExpiresAt: new Date('2015-01-01'), + }; + const client = { grants: ['password'] }; + const model = { + getClient() { + return client; + }, + revokeToken() { + return token; + }, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(e => { + e[0].should.be.an.instanceOf(InvalidTokenError); + e[1].should.be.an.instanceOf(InvalidTokenError); + response.body.should.eql({}); + response.status.should.equal(200); + }); + }); + + it('should return an empty object if successful', () => { + const token = { + refreshToken: 'hash', + client: {}, + user: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + }; + const client = { grants: ['password'] }; + const model = { + getClient() { + return client; + }, + revokeToken() { + return token; + }, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(data => { + should.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', () => { + it('should throw an error if `clientId` is invalid', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'øå€£‰', client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `clientId` is invalid', async () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + } + }); + + it('should throw an error if `client` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should throw an error if `client.grants` is missing', () => { + const model = { + getClient() { + return {}; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: missing client `grants`'); + }); + }); + + it('should throw a 401 error if the client is invalid and the request contains an authorization header', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getClient(request, response) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal('Invalid client: client is invalid'); + + response + .get('WWW-Authenticate') + .should.equal('Basic realm="Service"'); + }); + }); + + it('should return a client', () => { + const client = { id: 12345, grants: [] }; + const model = { + getClient() { + return client; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(data => { + data.should.equal(client); + }) + .catch(should.fail); + }); + + it('should support promises', () => { + const model = { + getClient() { + return Promise.resolve({ grants: [] }); + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + // it('should support callbacks', () => { + // const model = { + // getClient(clientId, clientSecret, callback) { + // callback(null, { grants: [] }); + // }, + // revokeToken() {}, + // getRefreshToken() {}, + // getAccessToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + // const request = new Request({ + // body: { client_id: 12345, client_secret: 'secret' }, + // headers: {}, + // method: 'ANY', + // query: {}, + // }); + + // handler.getClient(request).should.be.an.instanceOf(Promise); + // }); + + it('should support non-promises', () => { + const model = { + getClient() { + return { grants: [] }; + }, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + }); + + describe('getClientCredentials()', () => { + it('should throw an error if `client_id` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + it('should throw an error if `client_secret` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + describe('with `client_id` and `client_secret` in the request header as basic auth', () => { + it('should return a client', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request body', () => { + it('should return a client', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + }); + + describe('handleRevokeToken()', () => { + it('should throw an error if `token` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleRevokeToken(request) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should return a token', () => { + const client = { id: 12345, grants: ['password'] }; + const token = { + accessToken: 'hash', + client: { id: 12345 }, + accessTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }; + const model = { + getClient() {}, + revokeToken() { + return token; + }, + getRefreshToken() {}, + getAccessToken() { + return token; + }, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'hash' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleRevokeToken(request, client) + .then(data => { + should.exist(data); + }) + .catch(should.fail); + }); + + it('should return a token', () => { + const client = { id: 12345, grants: ['password'] }; + const token = { + refreshToken: 'hash', + client: { id: 12345 }, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }; + const model = { + getClient() {}, + revokeToken() { + return token; + }, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'hash' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleRevokeToken(request, client) + .then(data => { + should.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', () => { + it('should throw an error if the `refreshToken` is invalid', () => { + const client = {}; + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getRefreshToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: refresh token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', () => { + const client = { id: 'foo' }; + const token = { + refreshToken: 'hash', + client: { id: 'baz' }, + user: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getRefreshToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should return a token', () => { + const client = { id: 'foo' }; + const token = { + refreshToken: 'hash', + client: { id: 'foo' }, + user: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() { + return token; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getRefreshToken('hash', client) + .then(Token => { + should.exist(Token); + }) + .catch(should.fail); + }); + + // it('should support callbacks', () => { + // const client = { id: 'foo' }; + // const token = { + // refreshToken: 'hash', + // client: { id: 'foo' }, + // user: {}, + // refreshTokenExpiresAt: new Date(Date.now() * 2), + // }; + // const model = { + // getClient() {}, + // revokeToken() {}, + // getRefreshToken(refreshToken, callback) { + // callback(null, token); + // }, + // getAccessToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + + // return handler + // .getRefreshToken('hash', client) + // .then(token => { + // should.exist(token); + // }) + // .catch(should.fail); + // }); + }); + + describe('getAccessToken()', () => { + it('should throw an error if the `accessToken` is invalid', () => { + const client = {}; + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() {}, + getRefreshToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getAccessToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', () => { + const client = { id: 'foo' }; + const token = { + accessToken: 'hash', + client: { id: 'baz' }, + user: {}, + accessTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() { + return token; + }, + getRefreshToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getAccessToken('hash', client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should return a token', () => { + const client = { id: 'foo' }; + const token = { + accessToken: 'hash', + client: { id: 'foo' }, + user: {}, + accessTokenExpiresAt: new Date(Date.now() * 2), + }; + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() { + return token; + }, + getRefreshToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .getAccessToken('hash', client) + .then(Token => { + should.exist(Token); + }) + .catch(should.fail); + }); + + // it('should support callbacks', () => { + // const client = { id: 'foo' }; + // const token = { + // accessToken: 'hash', + // client: { id: 'foo' }, + // user: {}, + // accessTokenExpiresAt: new Date(Date.now() * 2), + // }; + // const model = { + // getClient() {}, + // revokeToken() {}, + // getAccessToken(accessToken, callback) { + // callback(null, token); + // }, + // getRefreshToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + + // return handler + // .getAccessToken('hash', client) + // .then(token => { + // should.exist(token); + // }) + // .catch(should.fail); + // }); + }); + + describe('revokeToken()', () => { + it('should throw an error if the `refreshToken` is invalid', () => { + const token = 'hash'; + const client = {}; + const model = { + getClient() {}, + revokeToken() { + return false; + }, + getRefreshToken() { + return { client: {}, user: {} }; + }, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + + return handler + .revokeToken(token, client) + .then(should.fail) + .catch(e => { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: token is invalid'); + }); + }); + + // it('should support callbacks', () => { + // const token = {}; + // const client = {}; + // const model = { + // getClient() {}, + // revokeToken(tokenObject, callback) { + // callback(null, null); + // }, + // getRefreshToken(refreshToken, callback) { + // callback(null, { client: {}, user: {} }); + // }, + // getAccessToken() {}, + // }; + // const handler:any = new RevokeHandler({ model }); + + // return handler + // .revokeToken(token, client) + // .then(should.fail) + // .catch(e => { + // e.should.be.an.instanceOf(InvalidTokenError); + // e.message.should.equal('Invalid token: token is invalid'); + // }); + // }); + }); + + describe('getTokenFromRequest()', () => { + it('should throw an error if `accessToken` is missing', () => { + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequest(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + } + }); + }); + + describe('updateErrorResponse()', () => { + it('should set the `body`', () => { + const error = new AccessDeniedError('Cannot request a revoke'); + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a revoke'); + }); + + it('should set the `status`', () => { + const error = new AccessDeniedError('Cannot request a revoke'); + const model = { + getClient() {}, + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); +}); diff --git a/test/integration/handlers/revoke-handler_test.js b/test/integration/handlers/revoke-handler_test.js deleted file mode 100644 index aeaf176c9..000000000 --- a/test/integration/handlers/revoke-handler_test.js +++ /dev/null @@ -1,860 +0,0 @@ -'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.spec.ts b/test/integration/handlers/token-handler.spec.ts new file mode 100755 index 000000000..25e861479 --- /dev/null +++ b/test/integration/handlers/token-handler.spec.ts @@ -0,0 +1,1670 @@ +import * as should from 'should'; +import * as util from 'util'; +import { + AccessDeniedError, + InvalidArgumentError, + InvalidClientError, + InvalidRequestError, + ServerError, + UnauthorizedClientError, + UnsupportedGrantTypeError, +} from '../../../lib/errors'; +import { PasswordGrantType } from '../../../lib/grant-types'; +import { TokenHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; +import { BearerTokenType } from '../../../lib/token-types'; + +/** + * Test `TokenHandler` integration. + */ + +describe('TokenHandler integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.accessTokenLifetime` is missing', () => { + try { + new TokenHandler(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', () => { + try { + new TokenHandler({ accessTokenLifetime: 120 }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if `options.refreshTokenLifetime` is missing', () => { + try { + new TokenHandler({ accessTokenLifetime: 120, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `refreshTokenLifetime`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', () => { + try { + new TokenHandler({ + accessTokenLifetime: 120, + model: {}, + refreshTokenLifetime: 120, + }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + it('should set the `accessTokenLifetime`', () => { + const accessTokenLifetime = {}; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime, + model, + refreshTokenLifetime: 120, + }); + + handler.accessTokenLifetime.should.equal(accessTokenLifetime); + }); + + it('should set the `alwaysIssueNewRefreshToken`', () => { + const alwaysIssueNewRefreshToken = true; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it('should set the `alwaysIssueNewRefreshToken` to false', () => { + const alwaysIssueNewRefreshToken = false; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it('should return the default `alwaysIssueNewRefreshToken` value', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 120, + }); + + handler.alwaysIssueNewRefreshToken.should.equal(true); + }); + + it('should set the `extendedGrantTypes`', () => { + const extendedGrantTypes = { foo: 'bar' }; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + extendedGrantTypes, + model, + refreshTokenLifetime: 120, + }); + + handler.grantTypes.should.containEql(extendedGrantTypes); + }); + + it('should set the `model`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.model.should.equal(model); + }); + + it('should set the `refreshTokenLifetime`', () => { + const refreshTokenLifetime = {}; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime, + }); + + handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); + }); + }); + + describe('handle()', () => { + it('should throw an error if `request` is missing', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + try { + await handler.handle(undefined, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handle(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if the method is not `POST`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'GET', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: method must be POST'); + }); + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + getUser() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it('should update the response if an error is thrown', () => { + const model = { + getClient() { + throw new Error('Unhandled exception'); + }, + getUser() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(() => { + response.body.should.eql({ + error: 'server_error', + error_description: 'Unhandled exception', + }); + response.status.should.equal(500); + }); + }); + + it('should return a bearer token if successful', async () => { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: 'foobar', + user: {}, + }; + const model = { + getClient() { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + try { + const data = await handler.handle(request, response); + data.should.eql(token); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', () => { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: 'foobar', + user: {}, + foo: 'bar', + }; + const model = { + getClient() { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(() => { + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + should.exist(response.body.scope); + should.not.exist(response.body.foo); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', async () => { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: 'foobar', + user: {}, + foo: 'bar', + }; + const model = { + getClient() { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + allowExtendedTokenAttributes: true, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + await handler.handle(request, response); + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + should.exist(response.body.scope); + should.exist(response.body.foo); + }); + }); + + describe('getClient()', () => { + it('should throw an error if `clientId` is invalid', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'øå€£‰', client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `clientSecret` is invalid', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'øå€£‰' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.getClient(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + } + }); + + it('should throw an error if `client` is missing', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should throw an error if `client.grants` is missing', () => { + const model = { + getClient() { + return {}; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: missing client `grants`'); + }); + }); + + it('should throw an error if `client.grants` is invalid', async () => { + const model = { + getClient() { + return { grants: 'foobar' }; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await handler.getClient(request, undefined); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `grants` must be an array'); + } + }); + + it('should throw a 401 error if the client is invalid and the request contains an authorization header', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getClient(request, response) + .then(() => { + should.fail('should.fail', ''); + }) + .catch(e => { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal('Invalid client: client is invalid'); + + response + .get('WWW-Authenticate') + .should.equal('Basic realm="Service"'); + }); + }); + + it('should return a client', async () => { + const client = { id: 12345, grants: [] }; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + const data = await handler.getClient(request, undefined); + data.should.equal(client); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + describe('with `password` grant type and `requireClientAuthentication` is false', () => { + it('should return a client ', () => { + const client = { id: 12345, grants: [] }; + const model = { + async getClient() { + return client; + }, + saveToken() {}, + }; + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { client_id: 'blah', grant_type: 'password' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(data => { + data.should.equal(client); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with `password` grant type and `requireClientAuthentication` is false and Authorization header', () => { + it('should return a client ', () => { + const client = { id: 12345, grants: [] }; + const model = { + async getClient() { + return client; + }, + saveToken() {}, + }; + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { grant_type: 'password' }, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('blah:').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, undefined) + .then(data => { + data.should.equal(client); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + it('should support promises', () => { + const model = { + getClient() { + return Promise.resolve({ grants: [] }); + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request, undefined).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', () => { + const model = { + getClient() { + return { grants: [] }; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request, undefined).should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', () => { + const model = { + getClient(clientId, clientSecret, callback) { + callback(null, { grants: [] }); + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + handler.getClient(request, undefined).should.be.an.instanceOf(Promise); + }); */ + }); + + describe('getClientCredentials()', () => { + it('should throw an error if `client_id` is missing', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_secret: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + it('should throw an error if `client_secret` is missing', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + handler.getClientCredentials(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', () => { + it('should return a client', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + requireClientAuthentication: { password: false }, + }); + const request = new Request({ + body: { client_id: 'foo', grant_type: 'password' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request header as basic auth', () => { + it('should return a client', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request body', () => { + it('should return a client', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + }); + + describe('handleGrantType()', () => { + it('should throw an error if `grant_type` is missing', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `grant_type`'); + } + }); + + it('should throw an error if `grant_type` is invalid', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: '~foo~' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, undefined); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `grant_type`'); + } + }); + + it('should throw an error if `grant_type` is unsupported', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'foobar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedGrantTypeError); + e.message.should.equal( + 'Unsupported grant type: `grant_type` is invalid', + ); + } + }); + + it('should throw an error if `grant_type` is unauthorized', async () => { + const client: any = { grants: ['client_credentials'] }; + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'password' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + try { + await handler.handleGrantType(request, client); + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal('Unauthorized client: `grant_type` is invalid'); + } + }); + + /* it('should throw an invalid grant error if a non-oauth error is thrown', () => { + const client = { grants: ['password'] }; + const model = { + getClient(clientId, password, callback) { + callback(null, client); + }, + getUser(uid, pwd, callback) { + callback(); + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'password', username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(() => should.fail('should.fail', '')) + .catch(e => { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + }); + }); */ + + describe('with grant_type `authorization_code`', () => { + it('should return a token', () => { + const client: any = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = { + getAuthorizationCode() { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + getClient() {}, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + revokeAuthorizationCode() { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler.handleGrantType(request, client).then(data => { + data.should.equal(token); + }); + // .catch(() => { + // should.fail('should.fail', ''); + // }); + }); + }); + + describe('with grant_type `client_credentials`', () => { + it('should return a token', () => { + const client: any = { grants: ['client_credentials'] }; + const token = {}; + const model = { + getClient() {}, + getUserFromClient() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: 'client_credentials', + scope: 'foo', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with grant_type `password`', () => { + it('should return a token', () => { + const client: any = { grants: ['password'] }; + const token = {}; + const model = { + getClient() {}, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'baz'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + scope: 'baz', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with grant_type `refresh_token`', () => { + it('should return a token', () => { + const client: any = { grants: ['refresh_token'] }; + const token = { accessToken: 'foo', client: {}, user: {} }; + const model = { + getClient() {}, + getRefreshToken() { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + }, + saveToken() { + return token; + }, + revokeToken() { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: 'refresh_token', + refresh_token: 12345, + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('with custom grant_type', () => { + it('should return a token', () => { + const client: any = { + grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'], + }; + const token = {}; + const model = { + getClient() {}, + getUser() { + return {}; + }, + saveToken() { + return token; + }, + validateScope() { + return 'foo'; + }, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + extendedGrantTypes: { + 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType, + }, + }); + const request = new Request({ + body: { + grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', + username: 'foo', + password: 'bar', + }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(data => { + data.should.equal(token); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + }); + + describe('getAccessTokenLifetime()', () => { + it('should return the client access token lifetime', () => { + const client: any = { accessTokenLifetime: 60 }; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(60); + }); + + it('should return the default access token lifetime', () => { + const client: any = {}; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(120); + }); + }); + + describe('getRefreshTokenLifetime()', () => { + it('should return the client access token lifetime', () => { + const client: any = { refreshTokenLifetime: 60 }; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(60); + }); + + it('should return the default access token lifetime', () => { + const client: any = {}; + const model = { + getClient() { + return client; + }, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(120); + }); + }); + + describe('getTokenType()', () => { + it('should return a token type', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = handler.getTokenType({ + accessToken: 'foo', + refreshToken: 'bar', + scope: 'foobar', + }); + + tokenType.should.containEql({ + accessToken: 'foo', + accessTokenLifetime: undefined, + refreshToken: 'bar', + scope: 'foobar', + }); + }); + }); + + describe('updateSuccessResponse()', () => { + it('should set the `body`', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.body.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + + it('should set the `Cache-Control` header', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get('Cache-Control').should.equal('no-store'); + }); + + it('should set the `Pragma` header', () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get('Pragma').should.equal('no-cache'); + }); + }); + + describe('updateErrorResponse()', () => { + it('should set the `body`', () => { + const error = new AccessDeniedError('Cannot request a token'); + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a token'); + }); + + it('should set the `status`', () => { + const error = new AccessDeniedError('Cannot request a token'); + const model = { + getClient() {}, + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); +}); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js deleted file mode 100644 index f3fd00ab5..000000000 --- a/test/integration/handlers/token-handler_test.js +++ /dev/null @@ -1,1080 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AccessDeniedError = require('../../../lib/errors/access-denied-error'); -var BearerTokenType = require('../../../lib/token-types/bearer-token-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var InvalidClientError = require('../../../lib/errors/invalid-client-error'); -var InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -var PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var ServerError = require('../../../lib/errors/server-error'); -var TokenHandler = require('../../../lib/handlers/token-handler'); -var UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); -var UnsupportedGrantTypeError = require('../../../lib/errors/unsupported-grant-type-error'); -var should = require('should'); -var util = require('util'); - -/** - * Test `TokenHandler` integration. - */ - -describe('TokenHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.accessTokenLifetime` is missing', function() { - try { - new TokenHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new TokenHandler({ accessTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if `options.refreshTokenLifetime` is missing', function() { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `refreshTokenLifetime`'); - } - }); - - it('should throw an error if the model does not implement `getClient()`', function() { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {}, refreshTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getClient()`'); - } - }); - - it('should set the `accessTokenLifetime`', function() { - var accessTokenLifetime = {}; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: accessTokenLifetime, model: model, refreshTokenLifetime: 120 }); - - handler.accessTokenLifetime.should.equal(accessTokenLifetime); - }); - - it('should set the `alwaysIssueNewRefreshToken`', function() { - var alwaysIssueNewRefreshToken = true; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); - - handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); - }); - - it('should set the `alwaysIssueNewRefreshToken` to false', function() { - var alwaysIssueNewRefreshToken = false; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); - - handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); - }); - - it('should return the default `alwaysIssueNewRefreshToken` value', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120 }); - - handler.alwaysIssueNewRefreshToken.should.equal(true); - }); - - it('should set the `extendedGrantTypes`', function() { - var extendedGrantTypes = { foo: 'bar' }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, extendedGrantTypes: extendedGrantTypes, model: model, refreshTokenLifetime: 120 }); - - handler.grantTypes.should.containEql(extendedGrantTypes); - }); - - it('should set the `model`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.model.should.equal(model); - }); - - it('should set the `refreshTokenLifetime`', function() { - var refreshTokenLifetime = {}; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: refreshTokenLifetime }); - - handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - try { - handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should throw an error if `response` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - }); - - it('should throw an error if the method is not `POST`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: 'GET', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: method must be POST'); - }); - }); - - it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); - }); - }); - - it('should throw the error if an oauth error is thrown', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - }); - }); - - it('should throw a server error if a non-oauth error is thrown', function() { - var model = { - getClient: function() { - throw new Error('Unhandled exception'); - }, - getUser: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Unhandled exception'); - e.inner.should.be.an.instanceOf(Error); - }); - }); - - it('should update the response if an error is thrown', function() { - var model = { - getClient: function() { - throw new Error('Unhandled exception'); - }, - getUser: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); - response.status.should.equal(500); - }); - }); - - 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'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - model.request.should.equal(request); - data.should.eql(token); - }) - .catch(should.fail); - }); - - it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', function() { - var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {}, foo: 'bar' }; - var model = { - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - should.exist(response.body.scope); - should.not.exist(response.body.foo); - }) - .catch(should.fail); - }); - - it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', function() { - var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {}, foo: 'bar' }; - var model = { - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, allowExtendedTokenAttributes: true }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - should.exist(response.body.scope); - should.exist(response.body.foo); - }) - .catch(should.fail); - }); - }); - - - describe('getClient()', function() { - it('should throw an error if `clientId` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - } - }); - - it('should throw an error if `clientSecret` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_secret`'); - } - }); - - it('should throw an error if `client` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client is invalid'); - }); - }); - - it('should throw an error if `client.grants` is missing', function() { - var model = { - getClient: function() { return {}; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: missing client `grants`'); - }); - }); - - it('should throw an error if `client.grants` is invalid', function() { - var model = { - getClient: function() { return { grants: 'foobar' }; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `grants` must be an array'); - }); - }); - - it('should throw a 401 error if the client is invalid and the request contains an authorization header', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: {}, - headers: { 'authorization': util.format('Basic %s', new Buffer('foo:bar').toString('base64')) }, - method: {}, - query: {} - }); - var response = new Response({ body: {}, headers: {} }); - - return handler.getClient(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.code.should.equal(401); - e.message.should.equal('Invalid client: client is invalid'); - - response.get('WWW-Authenticate').should.equal('Basic realm="Service"'); - }); - }); - - it('should return a client', function() { - var client = { id: 12345, grants: [] }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - - describe('with `password` grant type and `requireClientAuthentication` is false', function() { - - it('should return a client ', function() { - var client = { id: 12345, grants: [] }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - - var handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false - } - }); - var request = new Request({ body: { client_id: 'blah', grant_type: 'password'}, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - describe('with `password` grant type and `requireClientAuthentication` is false and Authorization header', function() { - - it('should return a client ', function() { - var client = { id: 12345, grants: [] }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - - var handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false - } - }); - var request = new Request({ - body: { grant_type: 'password'}, - headers: { 'authorization': util.format('Basic %s', new Buffer('blah:').toString('base64')) }, - method: {}, - query: {} - }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - it('should support promises', function() { - var model = { - getClient: function() { return Promise.resolve({ grants: [] }); }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getClient: function() { return { grants: [] }; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function() { - var model = { - getClient: function(clientId, clientSecret, callback) { callback(null, { grants: [] }); }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - }); - - describe('getClientCredentials()', function() { - it('should throw an error if `client_id` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - } - }); - - it('should throw an error if `client_secret` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - } - }); - - describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', function() { - it('should return a client', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, requireClientAuthentication: { password: false} }); - var request = new Request({ body: { client_id: 'foo', grant_type: 'password' }, headers: {}, method: {}, query: {} }); - var credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo' }); - }); - }); - - describe('with `client_id` and `client_secret` in the request header as basic auth', function() { - it('should return a client', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: {}, - headers: { - 'authorization': util.format('Basic %s', new Buffer('foo:bar').toString('base64')) - }, - method: {}, - query: {} - }); - var credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); - }); - }); - - describe('with `client_id` and `client_secret` in the request body', function() { - it('should return a client', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, query: {} }); - var credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); - }); - }); - }); - - describe('handleGrantType()', function() { - it('should throw an error if `grant_type` is missing', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `grant_type`'); - } - }); - - it('should throw an error if `grant_type` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: '~foo~' }, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `grant_type`'); - } - }); - - it('should throw an error if `grant_type` is unsupported', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedGrantTypeError); - e.message.should.equal('Unsupported grant type: `grant_type` is invalid'); - } - }); - - it('should throw an error if `grant_type` is unauthorized', function() { - var client = { grants: ['client_credentials'] }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, query: {} }); - - try { - handler.handleGrantType(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - } - }); - - it('should throw an invalid grant error if a non-oauth error is thrown', function() { - var client = { grants: ['password'] }; - var model = { - getClient: function(clientId, password, callback) { callback(null, client); }, - getUser: function(uid, pwd, callback) { callback(); }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'password', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - describe('with grant_type `authorization_code`', function() { - it('should return a token', function() { - var client = { id: 'foobar', grants: ['authorization_code'] }; - var token = {}; - var model = { - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - getClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; }, - revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `client_credentials`', function() { - it('should return a token', function() { - var client = { grants: ['client_credentials'] }; - var token = {}; - var model = { - getClient: function() {}, - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - grant_type: 'client_credentials', - scope: 'foo' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `password`', function() { - it('should return a token', function() { - var client = { grants: ['password'] }; - var token = {}; - var model = { - getClient: function() {}, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'baz'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo', - scope: 'baz' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `refresh_token`', function() { - it('should return a token', function() { - var client = { grants: ['refresh_token'] }; - var token = { accessToken: 'foo', client: {}, user: {} }; - var model = { - getClient: function() {}, - getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; }, - saveToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ - body: { - grant_type: 'refresh_token', - refresh_token: 12345 - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with custom grant_type', function() { - it('should return a token', function() { - var client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; - var token = {}; - var model = { - getClient: function() {}, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return 'foo'; } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, extendedGrantTypes: { 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType } }); - var request = new Request({ body: { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - }); - - describe('getAccessTokenLifetime()', function() { - it('should return the client access token lifetime', function() { - var client = { accessTokenLifetime: 60 }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getAccessTokenLifetime(client).should.equal(60); - }); - - it('should return the default access token lifetime', function() { - var client = {}; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getAccessTokenLifetime(client).should.equal(120); - }); - }); - - describe('getRefreshTokenLifetime()', function() { - it('should return the client access token lifetime', function() { - var client = { refreshTokenLifetime: 60 }; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getRefreshTokenLifetime(client).should.equal(60); - }); - - it('should return the default access token lifetime', function() { - var client = {}; - var model = { - getClient: function() { return client; }, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getRefreshTokenLifetime(client).should.equal(120); - }); - }); - - describe('getTokenType()', function() { - it('should return a token type', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); - - tokenType.should.containEql({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: 'foobar' }); - }); - }); - - describe('updateSuccessResponse()', function() { - it('should set the `body`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = new BearerTokenType('foo', 'bar', 'biz'); - var response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.body.should.eql({ access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', token_type: 'Bearer' }); - }); - - it('should set the `Cache-Control` header', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = new BearerTokenType('foo', 'bar', 'biz'); - var response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get('Cache-Control').should.equal('no-store'); - }); - - it('should set the `Pragma` header', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = new BearerTokenType('foo', 'bar', 'biz'); - var response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get('Pragma').should.equal('no-cache'); - }); - }); - - describe('updateErrorResponse()', function() { - it('should set the `body`', function() { - var error = new AccessDeniedError('Cannot request a token'); - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.body.error.should.equal('access_denied'); - response.body.error_description.should.equal('Cannot request a token'); - }); - - it('should set the `status`', function() { - var error = new AccessDeniedError('Cannot request a token'); - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.status.should.equal(400); - }); - }); -}); diff --git a/test/integration/request.spec.ts b/test/integration/request.spec.ts new file mode 100755 index 000000000..9515ed7b3 --- /dev/null +++ b/test/integration/request.spec.ts @@ -0,0 +1,183 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../lib/errors'; +import { Request } from '../../lib/request'; + +/** + * Test `Request` integration. + */ + +describe('Request integration', () => { + describe('constructor()', () => { + it('should throw an error if `headers` is missing', () => { + try { + new Request({ body: {} } as any); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `headers`'); + } + }); + + it('should throw an error if `method` is missing', () => { + try { + new Request({ body: {}, headers: {} } as any); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `method`'); + } + }); + + it('should throw an error if `query` is missing', () => { + try { + new Request({ body: {}, headers: {}, method: 'ANY' } as any); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `query`'); + } + }); + + it('should set the `body`', () => { + const request = new Request({ + body: 'foo', + headers: {}, + method: 'ANY', + query: {}, + }); + + request.body.should.equal('foo'); + }); + + it('should set the `headers`', () => { + const request = new Request({ + body: {}, + headers: { foo: 'bar', QuX: 'biz' }, + method: 'ANY', + query: {}, + }); + + request.headers.should.eql({ foo: 'bar', qux: 'biz' }); + }); + + it('should set the `method`', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'biz', + query: {}, + }); + + request.method.should.equal('BIZ'); + }); + + it('should set the `query`', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: 'baz', + }); + + request.query.should.equal('baz'); + }); + }); + + describe('get()', () => { + it('should return `undefined` if the field does not exist', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + (request.get('content-type') === undefined).should.be.true(); + }); + + it('should return the value if the field exists', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + }, + method: 'ANY', + query: {}, + }); + + request.get('Content-Type').should.equal('text/html; charset=utf-8'); + }); + }); + + describe('is()', () => { + it('should accept an array of `types`', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is(['html', 'json']).should.equal('json'); + }); + + it('should accept multiple `types` as arguments', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is('html', 'json').should.equal('json'); + }); + + it('should return the first matching type', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is('html').should.equal('html'); + }); + + it('should return `false` if none of the `types` match', () => { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'transfer-encoding': 'chunked', + }, + method: 'ANY', + query: {}, + }); + + request.is('json').should.be.false(); + }); + + it('should return `false` if the request has no body', () => { + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + + request.is('text/html').should.be.false(); + }); + }); +}); diff --git a/test/integration/request_test.js b/test/integration/request_test.js deleted file mode 100644 index 5233e1305..000000000 --- a/test/integration/request_test.js +++ /dev/null @@ -1,159 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Request = require('../../lib/request'); -var InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -var should = require('should'); - -/** - * Test `Request` integration. - */ - -describe('Request integration', function() { - describe('constructor()', function() { - it('should throw an error if `headers` is missing', function() { - try { - new Request({ body: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `headers`'); - } - }); - - it('should throw an error if `method` is missing', function() { - try { - new Request({ body: {}, headers: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `method`'); - } - }); - - it('should throw an error if `query` is missing', function() { - try { - new Request({ body: {}, headers: {}, method: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `query`'); - } - }); - - it('should set the `body`', function() { - var request = new Request({ body: 'foo', headers: {}, method: {}, query: {} }); - - request.body.should.equal('foo'); - }); - - it('should set the `headers`', function() { - var request = new Request({ body: {}, headers: { foo: 'bar', QuX: 'biz' }, method: {}, query: {} }); - - request.headers.should.eql({ foo: 'bar', qux: 'biz' }); - }); - - it('should set the `method`', function() { - var request = new Request({ body: {}, headers: {}, method: 'biz', query: {} }); - - request.method.should.equal('biz'); - }); - - it('should set the `query`', function() { - var request = new Request({ body: {}, headers: {}, method: {}, query: 'baz' }); - - request.query.should.equal('baz'); - }); - }); - - describe('get()', function() { - it('should return `undefined` if the field does not exist', function() { - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - (undefined === request.get('content-type')).should.be.true(); - }); - - it('should return the value if the field exists', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8' - }, - method: {}, - query: {} - }); - - request.get('Content-Type').should.equal('text/html; charset=utf-8'); - }); - }); - - describe('is()', function() { - it('should accept an array of `types`', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'application/json', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is(['html', 'json']).should.equal('json'); - }); - - it('should accept multiple `types` as arguments', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'application/json', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('html', 'json').should.equal('json'); - }); - - it('should return the first matching type', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('html').should.equal('html'); - }); - - it('should return `false` if none of the `types` match', function() { - var request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('json').should.be.false(); - }); - - it('should return `false` if the request has no body', function() { - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - request.is('text/html').should.be.false(); - }); - }); -}); diff --git a/test/integration/response-types/code-response-type.spec.ts b/test/integration/response-types/code-response-type.spec.ts new file mode 100755 index 000000000..1824a7f51 --- /dev/null +++ b/test/integration/response-types/code-response-type.spec.ts @@ -0,0 +1,339 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import * as url from 'url'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { CodeResponseType } from '../../../lib/response-types'; + +/** + * Test `CodeResponseType` integration. + */ + +describe('CodeResponseType integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.authorizationCodeLifetime` is missing', () => { + try { + new CodeResponseType(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Missing parameter: `authorizationCodeLifetime`', + ); + } + }); + + it('should set the `code`', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + responseType.authorizationCodeLifetime.should.equal(120); + }); + }); + + it('should throw an error if the model does not implement `saveAuthorizationCode()`', () => { + try { + new CodeResponseType({ authorizationCodeLifetime: 120, model: {} }); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveAuthorizationCode()`', + ); + } + }); + + it('should set the `authorizationCodeLifetime`', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler.authorizationCodeLifetime.should.equal(120); + }); + + describe('buildRedirectUri()', () => { + it('should throw an error if the `redirectUri` is missing', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + try { + responseType.buildRedirectUri(undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `redirectUri`'); + } + }); + + it('should return the new redirect uri and set the `code` and `state` in the query', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + responseType.code = 'foo'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb'), + ); + + url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); + }); + + it('should return the new redirect uri and append the `code` and `state` in the query', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const responseType = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + responseType.code = 'foo'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb?foo=bar', true), + ); + + url + .format(redirectUri) + .should.equal('http://example.com/cb?foo=bar&code=foo'); + }); + }); + + it('should set the `model`', () => { + const model = { + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler.model.should.equal(model); + }); + + describe('generateAuthorizationCode()', () => { + it('should return an auth code', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .generateAuthorizationCode(undefined, undefined, undefined) + .then((data: any) => { + data.should.be.a.sha1(); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises', () => { + const model = { + generateAuthorizationCode: () => { + return Promise.resolve({}); + }, + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .generateAuthorizationCode(undefined, undefined, undefined) + .should.be.an.instanceOf(Promise); + }); + + // it('should support non-promises', () => { + // const model = { + // generateAuthorizationCode: () => { + // return {}; + // }, + // getAccessToken: () => {}, + // getClient: () => {}, + // saveAuthorizationCode: () => {}, + // }; + // const handler = new CodeResponseType({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .generateAuthorizationCode(undefined, undefined, undefined) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('getAuthorizationCodeExpiresAt()', () => { + it('should return a date', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler: any = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler.getAuthorizationCodeExpiresAt({}).should.be.an.instanceOf(Date); + }); + }); + + describe('saveAuthorizationCode()', () => { + it('should return an auth code', () => { + const authorizationCode = {}; + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => { + return authorizationCode; + }, + }; + const handler: any = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + .then(data => { + data.should.equal(authorizationCode); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should support promises when calling `model.saveAuthorizationCode()`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => { + return Promise.resolve({}); + }, + }; + const handler: any = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz', undefined, undefined) + .should.be.an.instanceOf(Promise); + }); + + // it('should support non-promises when calling `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken: () => {}, + // getClient: () => {}, + // saveAuthorizationCode: () => { + // return {}; + // }, + // }; + // const handler = new CodeResponseType({ + // authorizationCodeLifetime: 120, + // model, + // }); + + // handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'biz', + // 'baz' as any, + // undefined, + // undefined, + // ) + // .should.be.an.instanceOf(Promise); + // }); + }); + + describe('saveAuthorizationCode()', () => { + it('should call `model.saveAuthorizationCode()`', () => { + const model = { + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: sinon.stub().returns({}), + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .saveAuthorizationCode( + 'foo', + 'bar' as any, + 'qux', + 'biz' as any, + 'baz', + 'boz' as any, + ) + .then(() => { + model.saveAuthorizationCode.callCount.should.equal(1); + model.saveAuthorizationCode.firstCall.args.should.have.length(3); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ + authorizationCode: 'foo', + expiresAt: 'bar', + redirectUri: 'baz', + scope: 'qux', + }); + model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('generateAuthorizationCode()', () => { + it('should call `model.generateAuthorizationCode()`', () => { + const model = { + generateAuthorizationCode: sinon.stub().returns({}), + getAccessToken: () => {}, + getClient: () => {}, + saveAuthorizationCode: () => {}, + }; + const handler = new CodeResponseType({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .generateAuthorizationCode(undefined, undefined, undefined) + .then(() => { + model.generateAuthorizationCode.callCount.should.equal(1); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js deleted file mode 100644 index 19f1d26ae..000000000 --- a/test/integration/response-types/code-response-type_test.js +++ /dev/null @@ -1,252 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -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'); - -/** - * Test `CodeResponseType` integration. - */ - -describe('CodeResponseType integration', function() { - describe('constructor()', 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: `authorizationCodeLifetime`'); - } - }); - - it('should set the `code`', function() { - var model = { - saveAuthorizationCode: function() {} - }; - var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: model }); - - 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 model = { - saveAuthorizationCode: function() {} - }; - var responseType = new CodeResponseType({ authorizationCodeLifetime: 120, model: 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 the `code` and `state` in the query', function() { - 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 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.spec.ts b/test/integration/response-types/token-response-type.spec.ts new file mode 100644 index 000000000..97847c8a6 --- /dev/null +++ b/test/integration/response-types/token-response-type.spec.ts @@ -0,0 +1,96 @@ +import * as should from 'should'; +import * as url from 'url'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { TokenResponseType } from '../../../lib/response-types'; + +/** + * Test `TokenResponseType` integration. + */ + +describe('TokenResponseType integration', () => { + describe('constructor()', () => { + it('should throw an error if `options.accessTokenLifetime` is missing', () => { + try { + new TokenResponseType(); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should set `accessTokenLifetime`', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + responseType.accessTokenLifetime.should.equal(120); + }); + + it('should set the `model`', () => { + const model = { + foobar() {}, + }; + const handler = new TokenResponseType({ + accessTokenLifetime: 120, + model, + }); + + handler.model.should.equal(model); + }); + }); + + describe('buildRedirectUri()', () => { + it('should throw an error if the `redirectUri` is missing', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + try { + responseType.buildRedirectUri(undefined); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `redirectUri`'); + } + }); + + it('should return the new redirect uri and set `access_token` and `state` in the query', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + responseType.accessToken = 'foobar-token'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb'), + ); + + url + .format(redirectUri) + .should.equal('http://example.com/cb#access_token=foobar-token'); + }); + + it('should return the new redirect uri and append `access_token` and `state` in the query', () => { + const responseType = new TokenResponseType({ + accessTokenLifetime: 120, + model: {}, + }); + + responseType.accessToken = 'foobar-token'; + const redirectUri = responseType.buildRedirectUri( + url.parse('http://example.com/cb?foo=bar', true), + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?foo=bar#access_token=foobar-token', + ); + }); + }); +}); diff --git a/test/integration/response-types/token-response-type_test.js b/test/integration/response-types/token-response-type_test.js deleted file mode 100644 index 7e3cc8b1e..000000000 --- a/test/integration/response-types/token-response-type_test.js +++ /dev/null @@ -1,89 +0,0 @@ -'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.spec.ts b/test/integration/response.spec.ts new file mode 100755 index 000000000..dc73475af --- /dev/null +++ b/test/integration/response.spec.ts @@ -0,0 +1,75 @@ +import { Response } from '../../lib/response'; + +/** + * Test `Response` integration. + */ + +describe('Response integration', () => { + describe('constructor()', () => { + it('should set the `body`', () => { + const response = new Response({ body: 'foo', headers: {} }); + + response.body.should.equal('foo'); + }); + + it('should set the `headers`', () => { + const response = new Response({ + body: {}, + headers: { foo: 'bar', QuX: 'biz' }, + }); + + response.headers.should.eql({ foo: 'bar', qux: 'biz' }); + }); + + it('should set the `status` to 200', () => { + const response = new Response({ body: {}, headers: {} }); + + response.status.should.equal(200); + }); + }); + + describe('get()', () => { + it('should return `undefined` if the field does not exist', () => { + const response = new Response({ body: {}, headers: {} }); + + (response.get('content-type') === undefined).should.be.true(); + }); + + it('should return the value if the field exists', () => { + const response = new Response({ + body: {}, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); + + response.get('Content-Type').should.equal('text/html; charset=utf-8'); + }); + }); + + describe('redirect()', () => { + it('should set the location header to `url`', () => { + const response = new Response({ body: {}, headers: {} }); + + response.redirect('http://example.com'); + + response.get('Location').should.equal('http://example.com'); + }); + + it('should set the `status` to 302', () => { + const response = new Response({ body: {}, headers: {} }); + + response.redirect('http://example.com'); + + response.status.should.equal(302); + }); + }); + + describe('set()', () => { + it('should set the `field`', () => { + const response = new Response({ body: {}, headers: {} }); + + response.set('foo', 'bar'); + + response.headers.should.eql({ foo: 'bar' }); + }); + }); +}); diff --git a/test/integration/response_test.js b/test/integration/response_test.js deleted file mode 100644 index 821069abf..000000000 --- a/test/integration/response_test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Response = require('../../lib/response'); - -/** - * Test `Response` integration. - */ - -describe('Response integration', function() { - describe('constructor()', function() { - it('should set the `body`', function() { - var response = new Response({ body: 'foo', headers: {} }); - - response.body.should.equal('foo'); - }); - - it('should set the `headers`', function() { - var response = new Response({ body: {}, headers: { foo: 'bar', QuX: 'biz' } }); - - response.headers.should.eql({ foo: 'bar', qux: 'biz' }); - }); - - it('should set the `status` to 200', function() { - var response = new Response({ body: {}, headers: {} }); - - response.status.should.equal(200); - }); - }); - - describe('get()', function() { - it('should return `undefined` if the field does not exist', function() { - var response = new Response({ body: {}, headers: {} }); - - (undefined === response.get('content-type')).should.be.true(); - }); - - it('should return the value if the field exists', function() { - var response = new Response({ body: {}, headers: { 'content-type': 'text/html; charset=utf-8' } }); - - response.get('Content-Type').should.equal('text/html; charset=utf-8'); - }); - }); - - describe('redirect()', function() { - it('should set the location header to `url`', function() { - var response = new Response({ body: {}, headers: {} }); - - response.redirect('http://example.com'); - - response.get('Location').should.equal('http://example.com'); - }); - - it('should set the `status` to 302', function() { - var response = new Response({ body: {}, headers: {} }); - - response.redirect('http://example.com'); - - response.status.should.equal(302); - }); - }); - - describe('set()', function() { - it('should set the `field`', function() { - var response = new Response({ body: {}, headers: {} }); - - response.set('foo', 'bar'); - - response.headers.should.eql({ foo: 'bar' }); - }); - }); -}); diff --git a/test/integration/server.spec.ts b/test/integration/server.spec.ts new file mode 100755 index 000000000..cda191909 --- /dev/null +++ b/test/integration/server.spec.ts @@ -0,0 +1,354 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { InvalidArgumentError } from '../../lib/errors'; +import { + AuthenticateHandler, + AuthorizeHandler, + TokenHandler, +} from '../../lib/handlers'; +import { Request } from '../../lib/request'; +import { Response } from '../../lib/response'; +import { OAuth2Server as Server } from '../../lib/server'; + +/** + * Test `Server` integration. + */ + +describe('Server integration', () => { + describe('constructor()', () => { + it('should throw an error if `model` is missing', () => { + try { + new Server({}); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should set the `model`', () => { + const model = {}; + const server = new Server({ model }); + + server.options.model.should.equal(model); + }); + }); + + describe('authenticate()', () => { + it('should set the default `options`', async () => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + try { + const stub = sinon + .stub(AuthenticateHandler.prototype, 'handle') + .returnsThis(); + const token = await server.authenticate(request, response); + token.addAcceptedScopesHeader.should.be.true(); + token.addAuthorizedScopesHeader.should.be.true(); + token.allowBearerTokensInQueryString.should.be.false(); + stub.restore(); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + it('should return a promise', () => { + const model = { + async getAccessToken(token) { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.authenticate(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + + /* it('should support callbacks', next => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + // server.authenticate(request, response, null, next); + }); */ + }); + + describe('authorize()', () => { + it('should set the default `options`', async () => { + const model = { + async getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + async getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + async saveAuthorizationCode() { + return { authorizationCode: 123 }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { state: 'foobar' }, + }); + const response = new Response({ body: {}, headers: {} }); + // try { + const stub = sinon + .stub(AuthorizeHandler.prototype, 'handle') + .returnsThis(); + const code = await server.authorize(request, response); + const options = code.options; + options.allowEmptyState.should.be.false(); + options.authorizationCodeLifetime.should.be.equal(300); + stub.restore(); + // } catch (error) { + // should.fail('should.fail', ''); + // } + }); + + it('should return a promise', () => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() { + return { authorizationCode: 123 }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { state: 'foobar' }, + }); + + const response = new Response({ body: {}, headers: {} }); + try { + const handler = server.authorize(request, response); + handler.should.be.an.instanceOf(Promise); + } catch (error) { + should.fail('should.fail', ''); + } + }); + + /* it('should support callbacks', next => { + const model = { + getAccessToken() { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient() { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode() { + return { authorizationCode: 123 }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: { state: 'foobar' }, + }); + const response = new Response({ body: {}, headers: {} }); + + // tslint:disable-next-line: no-floating-promises + // server.authorize(request, response, undefined, next); + }); */ + }); + + describe('token()', () => { + it('should set the default `options`', async () => { + const model = { + async getClient() { + return { grants: ['password'] }; + }, + async getUser() { + return {}; + }, + async saveToken() { + return { accessToken: 1234, client: {}, user: {} }; + }, + async validateScope() { + return 'foo'; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + grant_type: 'password', + username: 'foo', + password: 'pass', + scope: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const stub = sinon.stub(TokenHandler.prototype, 'handle').returnsThis(); + // try { + const token = await server.token(request, response); + token.accessTokenLifetime.should.equal(3600); + token.refreshTokenLifetime.should.equal(1209600); + stub.restore(); + // } catch (error) { + // should.fail('should.fail', ''); + // } + }); + + it('should return a promise', () => { + const model = { + async getClient() { + return { grants: ['password'] }; + }, + async getUser() { + return {}; + }, + async saveToken() { + return { accessToken: 1234, client: {}, user: {} }; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + grant_type: 'password', + username: 'foo', + password: 'pass', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + // try { + const handler = server.token(request, response); + + handler.should.be.an.instanceOf(Promise); + // } catch (error) { + // should.fail('should.fail', ''); + // } + }); + + /* it('should support callbacks', next => { + const model = { + async () { + return { grants: ['password'] }; + }, + getUser() { + return {}; + }, + saveToken() { + return { accessToken: 1234, client: {}, user: {} }; + }, + validateScope() { + return 'foo'; + }, + }; + const server = new Server({ model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + grant_type: 'password', + username: 'foo', + password: 'pass', + scope: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + // server.token(request, response, null, next); + }); */ + }); +}); diff --git a/test/integration/server_test.js b/test/integration/server_test.js deleted file mode 100644 index 72b9aa92a..000000000 --- a/test/integration/server_test.js +++ /dev/null @@ -1,295 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -var Promise = require('bluebird'); -var Request = require('../../lib/request'); -var Response = require('../../lib/response'); -var Server = require('../../lib/server'); -var should = require('should'); - -/** - * Test `Server` integration. - */ - -describe('Server integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new Server({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should set the `model`', function() { - var model = {}; - var server = new Server({ model: model }); - - server.options.model.should.equal(model); - }); - }); - - describe('authenticate()', function() { - it('should set the default `options`', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return server.authenticate(request, response) - .then(function() { - this.addAcceptedScopesHeader.should.be.true(); - this.addAuthorizedScopesHeader.should.be.true(); - this.allowBearerTokensInQueryString.should.be.false(); - }) - .catch(should.fail); - }); - - it('should return a promise', function() { - var model = { - getAccessToken: function(token, callback) { - callback(null, { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }); - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - var handler = server.authenticate(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function(next) { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var response = new Response({ body: {}, headers: {} }); - - server.authenticate(request, response, null, next); - }); - }); - - describe('authorize()', function() { - it('should set the default `options`', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - var response = new Response({ body: {}, headers: {} }); - - return server.authorize(request, response) - .then(function() { - this.allowEmptyState.should.be.false(); - }) - .catch(should.fail); - }); - - it('should return a promise', function() { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - var response = new Response({ body: {}, headers: {} }); - var handler = server.authorize(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function(next) { - var model = { - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - var response = new Response({ body: {}, headers: {} }); - - server.authorize(request, response, null, next); - }); - }); - - describe('token()', function() { - it('should set the default `options`', function() { - var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - }, - validateScope: function() { return 'foo'; } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass', scope: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - return server.token(request, response) - .then(function() { - this.accessTokenLifetime.should.equal(3600); - this.refreshTokenLifetime.should.equal(1209600); - }) - .catch(should.fail); - }); - - it('should return a promise', function() { - var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - var handler = server.token(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - - it('should support callbacks', function(next) { - var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - }, - validateScope: function() { - return 'foo'; - } - }; - var server = new Server({ model: model }); - var request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass', scope: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - var response = new Response({ body: {}, headers: {} }); - - server.token(request, response, null, next); - }); - }); - - 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/token-types/bearer-token-type.spec.ts b/test/integration/token-types/bearer-token-type.spec.ts new file mode 100755 index 000000000..44ef8ef11 --- /dev/null +++ b/test/integration/token-types/bearer-token-type.spec.ts @@ -0,0 +1,135 @@ +import * as should from 'should'; +import { InvalidArgumentError } from '../../../lib/errors'; +import { BearerTokenType } from '../../../lib/token-types'; + +/** + * Test `BearerTokenType` integration. + */ + +describe('BearerTokenType integration', () => { + describe('constructor()', () => { + it('should throw an error if `accessToken` is missing', () => { + try { + new BearerTokenType( + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + + it('should set the `accessToken`', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + undefined, + undefined, + undefined, + ); + + responseType.accessToken.should.equal('foo'); + }); + + it('should set the `accessTokenLifetime`', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + undefined, + undefined, + undefined, + ); + + responseType.accessTokenLifetime.should.equal('bar'); + }); + + it('should set the `refreshToken`', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + + responseType.refreshToken.should.equal('biz'); + }); + }); + + describe('valueOf()', () => { + it('should return the value representation', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + undefined, + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + token_type: 'Bearer', + }); + }); + + it('should not include the `expires_in` if not given', () => { + const responseType = new BearerTokenType( + 'foo', + undefined, + undefined, + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + token_type: 'Bearer', + }); + }); + + it('should set `refresh_token` if `refreshToken` is defined', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + + it('should set `expires_in` if `accessTokenLifetime` is defined', () => { + const responseType = new BearerTokenType( + 'foo', + 'bar' as any, + 'biz', + undefined, + undefined, + ); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + }); +}); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js deleted file mode 100644 index 3c1ef6bdd..000000000 --- a/test/integration/token-types/bearer-token-type_test.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var BearerTokenType = require('../../../lib/token-types/bearer-token-type'); -var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -var should = require('should'); - -/** - * Test `BearerTokenType` integration. - */ - -describe('BearerTokenType integration', function() { - describe('constructor()', function() { - it('should throw an error if `accessToken` is missing', function() { - try { - new BearerTokenType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessToken`'); - } - }); - - it('should set the `accessToken`', function() { - var responseType = new BearerTokenType('foo', 'bar'); - - responseType.accessToken.should.equal('foo'); - }); - - it('should set the `accessTokenLifetime`', function() { - var responseType = new BearerTokenType('foo', 'bar'); - - responseType.accessTokenLifetime.should.equal('bar'); - }); - - it('should set the `refreshToken`', function() { - var responseType = new BearerTokenType('foo', 'bar', 'biz'); - - responseType.refreshToken.should.equal('biz'); - }); - }); - - describe('valueOf()', function() { - it('should return the value representation', function() { - var responseType = new BearerTokenType('foo', 'bar'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - token_type: 'Bearer' - }); - }); - - it('should not include the `expires_in` if not given', function() { - var responseType = new BearerTokenType('foo'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - token_type: 'Bearer' - }); - }); - - it('should set `refresh_token` if `refreshToken` is defined', function() { - var responseType = new BearerTokenType('foo', 'bar', 'biz'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - refresh_token: 'biz', - token_type: 'Bearer' - }); - }); - - it('should set `expires_in` if `accessTokenLifetime` is defined', function() { - var responseType = new BearerTokenType('foo', 'bar', 'biz'); - var value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - refresh_token: 'biz', - token_type: 'Bearer' - }); - }); - }); -}); diff --git a/test/integration/utils/token-util.spec.ts b/test/integration/utils/token-util.spec.ts new file mode 100755 index 000000000..9a7ea1747 --- /dev/null +++ b/test/integration/utils/token-util.spec.ts @@ -0,0 +1,19 @@ +import * as should from 'should'; +import * as TokenUtil from '../../../lib/utils/token-util'; + +/** + * Test `TokenUtil` integration. + */ + +describe('TokenUtil integration', () => { + describe('generateRandomToken()', () => { + it('should return a sha-1 token', async () => { + try { + const token: any = await TokenUtil.GenerateRandomToken(); + token.should.be.a.sha1(); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js deleted file mode 100644 index 16cca94fb..000000000 --- a/test/integration/utils/token-util_test.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var TokenUtil = require('../../../lib/utils/token-util'); -var should = require('should'); - -/** - * Test `TokenUtil` integration. - */ - -describe('TokenUtil integration', function() { - describe('generateRandomToken()', function() { - it('should return a sha-1 token', function() { - return TokenUtil.generateRandomToken() - .then(function(token) { - token.should.be.a.sha1(); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 00ecb38eb..000000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---require should ---require test/assertions ---ui bdd ---reporter spec diff --git a/test/tslint.json b/test/tslint.json new file mode 100755 index 000000000..8c62b6de9 --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,28 @@ +{ + "extends": ["tslint:recommended", "tslint:all", "tslint:latest"], + "jsRules": { + "no-unused-expression": true + }, + "rules": { + "file-name-casing": [true, "kebab-case"], + "no-unused-expression": false, + "semicolon": [true, "always", "ignore-bound-class-methods"], + "completed-docs": false, + "quotemark": [true, "single"], + "max-file-line-count": [false], + "member-access": [false], + "no-unsafe-any": false, + "promise-function-async": false, + "ordered-imports": [false], + "no-empty": [false], + "newline-per-chained-call": [false], + "max-line-length": [true, 120], + "no-magic-numbers": [false], + "member-ordering": [false], + "interface-name": [false], + "arrow-parens": false, + "no-implicit-dependencies": false, + "object-literal-sort-keys": false + }, + "rulesDirectory": [] +} diff --git a/test/unit/grant-types/abstract-grant-type.spec.ts b/test/unit/grant-types/abstract-grant-type.spec.ts new file mode 100755 index 000000000..12ab50977 --- /dev/null +++ b/test/unit/grant-types/abstract-grant-type.spec.ts @@ -0,0 +1,53 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { AbstractGrantType } from '../../../lib/grant-types'; + +/** + * Test `AbstractGrantType`. + */ + +describe('AbstractGrantType', () => { + describe('generateAccessToken()', () => { + it('should call `model.generateAccessToken()`', async () => { + const model = { + generateAccessToken: sinon + .stub() + .returns({ client: {}, expiresAt: new Date(), user: {} }), + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model, + }); + try { + await handler.generateAccessToken(); + model.generateAccessToken.callCount.should.equal(1); + model.generateAccessToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('generateRefreshToken()', () => { + it('should call `model.generateRefreshToken()`', async () => { + const model = { + generateRefreshToken: sinon.stub().returns({ + client: {}, + expiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model, + }); + try { + await handler.generateRefreshToken(); + model.generateRefreshToken.callCount.should.equal(1); + model.generateRefreshToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js deleted file mode 100644 index 528ca4041..000000000 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `AbstractGrantType`. - */ - -describe('AbstractGrantType', function() { - describe('generateAccessToken()', function() { - it('should call `model.generateAccessToken()`', function() { - var model = { - generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }) - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); - - return handler.generateAccessToken() - .then(function() { - model.generateAccessToken.callCount.should.equal(1); - model.generateAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('generateRefreshToken()', function() { - it('should call `model.generateRefreshToken()`', function() { - var model = { - generateRefreshToken: sinon.stub().returns({ client: {}, expiresAt: new Date(new Date() / 2), user: {} }) - }; - var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); - - return handler.generateRefreshToken() - .then(function() { - model.generateRefreshToken.callCount.should.equal(1); - model.generateRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/authorization-code-grant-type.spec.ts b/test/unit/grant-types/authorization-code-grant-type.spec.ts new file mode 100755 index 000000000..9ff43eaa0 --- /dev/null +++ b/test/unit/grant-types/authorization-code-grant-type.spec.ts @@ -0,0 +1,117 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { AuthorizationCodeGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `AuthorizationCodeGrantType`. + */ + +describe('AuthorizationCodeGrantType', () => { + describe('getAuthorizationCode()', () => { + it('should call `model.getAuthorizationCode()`', async () => { + const model = { + getAuthorizationCode: sinon.stub().returns({ + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }), + revokeAuthorizationCode() {}, + saveToken() {}, + }; + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client: any = {}; + try { + await handler.getAuthorizationCode(request, client); + + model.getAuthorizationCode.callCount.should.equal(1); + model.getAuthorizationCode.firstCall.args.should.have.length(1); + model.getAuthorizationCode.firstCall.args[0].should.equal(12345); + model.getAuthorizationCode.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('revokeAuthorizationCode()', () => { + it('should call `model.revokeAuthorizationCode()`', async () => { + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode: sinon.stub().returns(true), + saveToken() {}, + }; + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model, + }); + const authorizationCode: any = {}; + try { + await handler.revokeAuthorizationCode(authorizationCode); + + model.revokeAuthorizationCode.callCount.should.equal(1); + model.revokeAuthorizationCode.firstCall.args.should.have.length(1); + model.revokeAuthorizationCode.firstCall.args[0].should.equal( + authorizationCode, + ); + model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', async () => { + const client: any = {}; + const user = {}; + const model = { + getAuthorizationCode() {}, + revokeAuthorizationCode() {}, + saveToken: sinon.stub().returns(true), + }; + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'validateScope').returns('foobiz' as any); + sinon + .stub(handler, 'generateAccessToken') + .returns(Promise.resolve('foo')); + sinon + .stub(handler, 'generateRefreshToken') + .returns(Promise.resolve('bar')); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + try { + await handler.saveToken(user, client, 'foobar', 'foobiz'); + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + authorizationCode: 'foobar', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: 'foobiz', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js deleted file mode 100644 index 480416e68..000000000 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); -var Promise = require('bluebird'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `AuthorizationCodeGrantType`. - */ - -describe('AuthorizationCodeGrantType', function() { - describe('getAuthorizationCode()', function() { - it('should call `model.getAuthorizationCode()`', function() { - var model = { - getAuthorizationCode: sinon.stub().returns({ authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), user: {} }), - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }; - var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - var client = {}; - - return handler.getAuthorizationCode(request, client) - .then(function() { - model.getAuthorizationCode.callCount.should.equal(1); - model.getAuthorizationCode.firstCall.args.should.have.length(1); - model.getAuthorizationCode.firstCall.args[0].should.equal(12345); - model.getAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should call `model.revokeAuthorizationCode()`', function() { - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: sinon.stub().returns(true), - saveToken: function() {} - }; - var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - var authorizationCode = {}; - - return handler.revokeAuthorizationCode(authorizationCode) - .then(function() { - model.revokeAuthorizationCode.callCount.should.equal(1); - model.revokeAuthorizationCode.firstCall.args.should.have.length(1); - model.revokeAuthorizationCode.firstCall.args[0].should.equal(authorizationCode); - model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'validateScope').returns('foobiz'); - sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foo')); - sinon.stub(handler, 'generateRefreshToken').returns(Promise.resolve('bar')); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns(Promise.resolve('biz')); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns(Promise.resolve('baz')); - - return handler.saveToken(user, client, 'foobar', 'foobiz') - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', authorizationCode: 'foobar', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobiz' }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/client-credentials-grant-type.spec.ts b/test/unit/grant-types/client-credentials-grant-type.spec.ts new file mode 100755 index 000000000..693c7d669 --- /dev/null +++ b/test/unit/grant-types/client-credentials-grant-type.spec.ts @@ -0,0 +1,66 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ClientCredentialsGrantType } from '../../../lib/grant-types'; + +/** + * Test `ClientCredentialsGrantType`. + */ + +describe('ClientCredentialsGrantType', () => { + describe('getUserFromClient()', () => { + it('should call `model.getUserFromClient()`', async () => { + const model = { + getUserFromClient: sinon.stub().returns(true), + saveToken() {}, + }; + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + const client: any = {}; + try { + await handler.getUserFromClient(client); + model.getUserFromClient.callCount.should.equal(1); + model.getUserFromClient.firstCall.args.should.have.length(1); + model.getUserFromClient.firstCall.args[0].should.equal(client); + model.getUserFromClient.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', async () => { + const client: any = {}; + const user = {}; + const model = { + getUserFromClient() {}, + saveToken: sinon.stub().returns(true), + }; + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'validateScope').returns('foobar' as any); + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + try { + await handler.saveToken(user, client, 'foobar'); + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + scope: 'foobar', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js deleted file mode 100644 index fe1fc4840..000000000 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `ClientCredentialsGrantType`. - */ - -describe('ClientCredentialsGrantType', function() { - describe('getUserFromClient()', function() { - it('should call `model.getUserFromClient()`', function() { - var model = { - getUserFromClient: sinon.stub().returns(true), - saveToken: function() {} - }; - var handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - var client = {}; - - return handler.getUserFromClient(client) - .then(function() { - model.getUserFromClient.callCount.should.equal(1); - model.getUserFromClient.firstCall.args.should.have.length(1); - model.getUserFromClient.firstCall.args[0].should.equal(client); - model.getUserFromClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getUserFromClient: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'validateScope').returns('foobar'); - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - - return handler.saveToken(user, client, 'foobar') - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: 'foobar' }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/implicit-grant-type_test.js b/test/unit/grant-types/implicit-grant-type_test.js deleted file mode 100644 index dc100933c..000000000 --- a/test/unit/grant-types/implicit-grant-type_test.js +++ /dev/null @@ -1,50 +0,0 @@ -'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/grant-types/implict-grant-type.spec.ts b/test/unit/grant-types/implict-grant-type.spec.ts new file mode 100644 index 000000000..504f693e3 --- /dev/null +++ b/test/unit/grant-types/implict-grant-type.spec.ts @@ -0,0 +1,45 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ImplicitGrantType } from '../../../lib/grant-types'; +/** + * Test `ImplicitGrantType`. + */ + +describe('ImplicitGrantType', () => { + describe('saveToken()', () => { + it('should call `model.saveToken()`', () => { + const client = {}; + const user = {}; + const model = { + saveToken: sinon.stub().returns(true), + }; + const handler: any = new ImplicitGrantType({ + accessTokenLifetime: 120, + model, + user, + }); + + sinon.stub(handler, 'validateScope').returns('foobar-scope'); + sinon + .stub(handler, 'generateAccessToken') + .returns(Promise.resolve('foobar-token')); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('foo-1234'); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foobar-token', + accessTokenExpiresAt: 'foo-1234', + scope: 'foobar-scope', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/grant-types/password-grant-type.spec.ts b/test/unit/grant-types/password-grant-type.spec.ts new file mode 100755 index 000000000..c4f77beff --- /dev/null +++ b/test/unit/grant-types/password-grant-type.spec.ts @@ -0,0 +1,78 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { PasswordGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `PasswordGrantType`. + */ + +describe('PasswordGrantType', () => { + describe('getUser()', () => { + it('should call `model.getUser()`', async () => { + const model = { + getUser: sinon.stub().returns(true), + saveToken() {}, + }; + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await handler.getUser(request); + model.getUser.callCount.should.equal(1); + model.getUser.firstCall.args.should.have.length(2); + model.getUser.firstCall.args[0].should.equal('foo'); + model.getUser.firstCall.args[1].should.equal('bar'); + model.getUser.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', async () => { + const client: any = {}; + const user = {}; + const model = { + getUser() {}, + saveToken: sinon.stub().returns(true), + }; + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'validateScope').returns('foobar' as any); + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'generateRefreshToken').returns('bar' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + try { + await handler.saveToken(user, client, 'foobar'); + + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: 'foobar', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); +}); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js deleted file mode 100644 index 8e3bfc84e..000000000 --- a/test/unit/grant-types/password-grant-type_test.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `PasswordGrantType`. - */ - -describe('PasswordGrantType', function() { - describe('getUser()', function() { - it('should call `model.getUser()`', function() { - var model = { - getUser: sinon.stub().returns(true), - saveToken: function() {} - }; - var handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.getUser(request) - .then(function() { - model.getUser.callCount.should.equal(1); - model.getUser.firstCall.args.should.have.length(2); - model.getUser.firstCall.args[0].should.equal('foo'); - model.getUser.firstCall.args[1].should.equal('bar'); - model.getUser.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getUser: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'validateScope').returns('foobar'); - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, 'foobar') - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/grant-types/refresh-token-grant-type.spec.ts b/test/unit/grant-types/refresh-token-grant-type.spec.ts new file mode 100755 index 000000000..6be1ed92c --- /dev/null +++ b/test/unit/grant-types/refresh-token-grant-type.spec.ts @@ -0,0 +1,298 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { RefreshTokenGrantType } from '../../../lib/grant-types'; +import { Request } from '../../../lib/request'; + +/** + * Test `RefreshTokenGrantType`. + */ + +describe('RefreshTokenGrantType', () => { + describe('handle()', () => { + it('should revoke the previous token', () => { + const token = { accessToken: 'foo', client: {}, user: {} }; + const model = { + getRefreshToken() { + return token; + }, + saveToken() { + return { accessToken: 'bar', client: {}, user: {} }; + }, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client: any = {}; + + return handler + .handle(request, client) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('getRefreshToken()', () => { + it('should call `model.getRefreshToken()`', () => { + const model = { + getRefreshToken: sinon + .stub() + .returns({ accessToken: 'foo', client: {}, user: {} }), + saveToken() {}, + revokeToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'bar' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client: any = {}; + + return handler + .getRefreshToken(request, client) + .then(() => { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal('bar'); + model.getRefreshToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('revokeToken()', () => { + it('should call `model.revokeToken()`', () => { + const model = { + getRefreshToken() {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + saveToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const token: any = {}; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should not call `model.revokeToken()`', () => { + const model = { + getRefreshToken() {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + saveToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: false, + }); + const token: any = {}; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(0); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should not call `model.revokeToken()`', () => { + const model = { + getRefreshToken() {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date().getTime() / 2), + user: {}, + }), + saveToken() {}, + }; + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: true, + }); + const token: any = {}; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('saveToken()', () => { + it('should call `model.saveToken()`', () => { + const client: any = {}; + const user = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken: sinon.stub().returns(true), + }; + const handler: any = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + sinon.stub(handler, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: 'foobar', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should call `model.saveToken()` without refresh token', () => { + const client = {}; + const user = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken: sinon.stub().returns(true), + }; + const handler: any = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: false, + }); + + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'generateRefreshToken').returns('bar' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + scope: 'foobar', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + + it('should call `model.saveToken()` with refresh token', () => { + const client = {}; + const user = {}; + const model = { + getRefreshToken() {}, + revokeToken() {}, + saveToken: sinon.stub().returns(true), + }; + const handler: any = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + alwaysIssueNewRefreshToken: true, + }); + + sinon.stub(handler, 'generateAccessToken').returns('foo' as any); + sinon.stub(handler, 'generateRefreshToken').returns('bar' as any); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz' as any); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz' as any); + + return handler + .saveToken(user, client, 'foobar') + .then(() => { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: 'foobar', + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js deleted file mode 100644 index e5693ba8f..000000000 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ /dev/null @@ -1,200 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); -var Request = require('../../../lib/request'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `RefreshTokenGrantType`. - */ - -describe('RefreshTokenGrantType', function() { - describe('handle()', function() { - it('should revoke the previous token', function() { - var token = { accessToken: 'foo', client: {}, user: {} }; - var model = { - getRefreshToken: function() { return token; }, - saveToken: function() { return { accessToken: 'bar', client: {}, user: {} }; }, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); - var client = {}; - - return handler.handle(request, client) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getRefreshToken()', function() { - it('should call `model.getRefreshToken()`', function() { - var model = { - getRefreshToken: sinon.stub().returns({ accessToken: 'foo', client: {}, user: {} }), - saveToken: function() {}, - revokeToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); - var client = {}; - - return handler.getRefreshToken(request, client) - .then(function() { - model.getRefreshToken.callCount.should.equal(1); - model.getRefreshToken.firstCall.args.should.have.length(1); - model.getRefreshToken.firstCall.args[0].should.equal('bar'); - model.getRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('revokeToken()', function() { - it('should call `model.revokeToken()`', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - var token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should not call `model.revokeToken()`', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); - var token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(0); - }) - .catch(should.fail); - }); - - it('should not call `model.revokeToken()`', function() { - var model = { - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true }); - var token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var client = {}; - var user = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, 'foobar') - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveToken()` without refresh token', function() { - var client = {}; - var user = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); - - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, 'foobar') - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: 'foobar' }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveToken()` with refresh token', function() { - var client = {}; - var user = {}; - var model = { - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }; - var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true}); - - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, 'foobar') - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/handlers/authenticate-handler.spec.ts b/test/unit/handlers/authenticate-handler.spec.ts new file mode 100755 index 000000000..442ef0f3c --- /dev/null +++ b/test/unit/handlers/authenticate-handler.spec.ts @@ -0,0 +1,166 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { ServerError } from '../../../lib/errors'; +import { AuthenticateHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; + +/** + * Test `AuthenticateHandler`. + */ + +describe('AuthenticateHandler', () => { + describe('getTokenFromRequest()', () => { + describe('with bearer token in the request authorization header', () => { + it('should call `getTokenFromRequestHeader()`', () => { + const handler: any = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: 'ANY', + query: {}, + }); + + sinon.stub(handler, 'getTokenFromRequestHeader'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestHeader.callCount.should.equal(1); + handler.getTokenFromRequestHeader.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestHeader.restore(); + }); + }); + + describe('with bearer token in the request query', () => { + it('should call `getTokenFromRequestQuery()`', () => { + const handler: any = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: { access_token: 'foo' }, + }); + + sinon.stub(handler, 'getTokenFromRequestQuery'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestQuery.callCount.should.equal(1); + handler.getTokenFromRequestQuery.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestQuery.restore(); + }); + }); + + describe('with bearer token in the request body', () => { + it('should call `getTokenFromRequestBody()`', () => { + const handler: any = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + sinon.stub(handler, 'getTokenFromRequestBody'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestBody.callCount.should.equal(1); + handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); + handler.getTokenFromRequestBody.restore(); + }); + }); + }); + + describe('getAccessToken()', () => { + it('should call `model.getAccessToken()`', () => { + const model = { + getAccessToken: sinon.stub().returns({ user: {} }), + }; + const handler = new AuthenticateHandler({ model }); + + return handler + .getAccessToken('foo') + .then(() => { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args.should.have.length(1); + model.getAccessToken.firstCall.args[0].should.equal('foo'); + model.getAccessToken.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + describe('validateAccessToken()', () => { + it('should fail if token has no valid `accessTokenExpiresAt` date', () => { + const model = { + getAccessToken() {}, + }; + const handler = new AuthenticateHandler({ model }); + + let failed = false; + try { + handler.validateAccessToken({ + user: {}, + } as any); + } catch (err) { + err.should.be.an.instanceOf(ServerError); + failed = true; + } + failed.should.equal(true); + }); + + it('should succeed if token has valid `accessTokenExpiresAt` date', () => { + const model = { + getAccessToken() {}, + }; + const handler = new AuthenticateHandler({ model }); + try { + handler.validateAccessToken({ + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + } as any); + } catch (err) { + should.fail('should.fail', ''); + } + }); + }); + + describe('verifyScope()', () => { + it('should call `model.getAccessToken()` if scope is defined', () => { + const model = { + getAccessToken() {}, + verifyScope: sinon.stub().returns(true), + }; + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model, + scope: 'bar', + }); + + return handler + .verifyScope('foo' as any) + .then(() => { + model.verifyScope.callCount.should.equal(1); + model.verifyScope.firstCall.args.should.have.length(2); + model.verifyScope.firstCall.args[0].should.equal('foo', 'bar'); + model.verifyScope.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js deleted file mode 100644 index 5c89f3d1f..000000000 --- a/test/unit/handlers/authenticate-handler_test.js +++ /dev/null @@ -1,187 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -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'); - -/** - * Test `AuthenticateHandler`. - */ - -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() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - - sinon.stub(handler, 'getTokenFromRequestHeader'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestHeader.callCount.should.equal(1); - handler.getTokenFromRequestHeader.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestHeader.restore(); - }); - }); - - describe('with bearer token in the request query', function() { - it('should call `getTokenFromRequestQuery()`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { access_token: 'foo' } - }); - - sinon.stub(handler, 'getTokenFromRequestQuery'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestQuery.callCount.should.equal(1); - handler.getTokenFromRequestQuery.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestQuery.restore(); - }); - }); - - describe('with bearer token in the request body', function() { - it('should call `getTokenFromRequestBody()`', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - var request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: {}, - query: {} - }); - - sinon.stub(handler, 'getTokenFromRequestBody'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestBody.callCount.should.equal(1); - handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestBody.restore(); - }); - }); - }); - - describe('getAccessToken()', function() { - it('should call `model.getAccessToken()`', function() { - var model = { - getAccessToken: sinon.stub().returns({ user: {} }) - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(function() { - model.getAccessToken.callCount.should.equal(1); - model.getAccessToken.firstCall.args.should.have.length(1); - model.getAccessToken.firstCall.args[0].should.equal('foo'); - model.getAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('validateAccessToken()', function() { - it('should fail if token has no valid `accessTokenExpiresAt` date', function() { - var model = { - getAccessToken: function() {} - }; - var handler = new AuthenticateHandler({ model: model }); - - var failed = false; - try { - handler.validateAccessToken({ - user: {} - }); - } - catch (err) { - err.should.be.an.instanceOf(ServerError); - failed = true; - } - failed.should.equal(true); - }); - - it('should succeed if token has valid `accessTokenExpiresAt` date', function() { - var model = { - getAccessToken: function() {} - }; - var handler = new AuthenticateHandler({ model: model }); - try { - handler.validateAccessToken({ - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }); - } - catch (err) { - should.fail(); - } - }); - }); - - describe('verifyScope()', function() { - it('should call `model.getAccessToken()` if scope is defined', function() { - var model = { - getAccessToken: function() {}, - verifyScope: sinon.stub().returns(true) - }; - var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'bar' }); - - return handler.verifyScope('foo') - .then(function() { - model.verifyScope.callCount.should.equal(1); - model.verifyScope.firstCall.args.should.have.length(2); - model.verifyScope.firstCall.args[0].should.equal('foo', 'bar'); - model.verifyScope.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/handlers/authorize-handler.spec.ts b/test/unit/handlers/authorize-handler.spec.ts new file mode 100755 index 000000000..04c010f08 --- /dev/null +++ b/test/unit/handlers/authorize-handler.spec.ts @@ -0,0 +1,140 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { AuthorizeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; +import { Response } from '../../../lib/response'; + +/** + * Test `AuthorizeHandler`. + */ + +describe('AuthorizeHandler', () => { + // describe('generateAuthorizationCode()', () => { + // it('should call `model.generateAuthorizationCode()`', async () => { + // const model = { + // generateAuthorizationCode: sinon.stub().returns({}), + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode() {}, + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // try { + // await handler.generateAuthorizationCode({}, {}, {}); + // model.generateAuthorizationCode.callCount.should.equal(1); + // model.generateAuthorizationCode.firstCall.thisValue.should.equal(model); + // } catch (error) { + // should.fail('should.fail', ''); + // } + // }); + // }); + + describe('getClient()', () => { + it('should call `model.getClient()`', async () => { + const model = { + getAccessToken() {}, + getClient: sinon.stub().returns( + Promise.resolve({ + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }), + ), + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + try { + await handler.getClient(request); + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(1); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.thisValue.should.equal(model); + } catch (error) { + should.fail('should.fail', ''); + } + }); + }); + + describe('getUser()', () => { + it('should call `authenticateHandler.getUser()`', () => { + const authenticateHandler = { + handle: sinon.stub().returns(Promise.resolve({})), + }; + const model = { + getClient() {}, + saveAuthorizationCode() {}, + }; + const handler = new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'ANY', + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(() => { + authenticateHandler.handle.callCount.should.equal(1); + authenticateHandler.handle.firstCall.args.should.have.length(2); + authenticateHandler.handle.firstCall.args[0].should.equal(request); + authenticateHandler.handle.firstCall.args[1].should.equal(response); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); + + // describe('saveAuthorizationCode()', () => { + // it('should call `model.saveAuthorizationCode()`', () => { + // const model = { + // getAccessToken() {}, + // getClient() {}, + // saveAuthorizationCode: sinon.stub().returns({}), + // }; + // const handler = new AuthorizeHandler({ + // authorizationCodeLifetime: 120, + // model, + // }); + // return handler + // .saveAuthorizationCode( + // 'foo', + // 'bar' as any, + // 'qux', + // 'biz' as any, + // 'baz', + // 'boz' as any, + // ) + // .then(() => { + // model.saveAuthorizationCode.callCount.should.equal(1); + // model.saveAuthorizationCode.firstCall.args.should.have.length(3); + // model.saveAuthorizationCode.firstCall.args[0].should.eql({ + // authorizationCode: 'foo', + // expiresAt: 'bar', + // redirectUri: 'baz', + // scope: 'qux', + // }); + // model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); + // model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + // model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + // }) + // .catch(() => should.fail('should.fail', '')); + // }); + // }); +}); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js deleted file mode 100644 index b172a8ca4..000000000 --- a/test/unit/handlers/authorize-handler_test.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); -var Request = require('../../../lib/request'); -var Response = require('../../../lib/response'); -var Promise = require('bluebird'); -var sinon = require('sinon'); -var should = require('should'); - -/** - * Test `AuthorizeHandler`. - */ - -describe('AuthorizeHandler', function() { - describe('handle()', function() { - it('should extend model object with request context', function() { - var model = { - getClient: sinon.stub().returns({ - grants: ['authorization_code'], - redirectUris: ['/abc'] - }), - saveAuthorizationCode: sinon.stub().returns({ authorizationCode: 'code_abc' }) - }; - 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.request.should.equal(request); - }) - .catch(should.fail); - }); - }); - - describe('getClient()', function() { - it('should call `model.getClient()`', function() { - var model = { - getAccessToken: function() {}, - getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }), - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getUser()', function() { - it('should call `authenticateHandler.getUser()`', function() { - var authenticateHandler = { handle: sinon.stub().returns(Promise.resolve({})) }; - var model = { - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); - var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - var response = new Response(); - - return handler.getUser(request, response) - .then(function() { - authenticateHandler.handle.callCount.should.equal(1); - authenticateHandler.handle.firstCall.args.should.have.length(2); - authenticateHandler.handle.firstCall.args[0].should.equal(request); - authenticateHandler.handle.firstCall.args[1].should.equal(response); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/handlers/revoke-handler_test.js b/test/unit/handlers/revoke-handler_test.js deleted file mode 100644 index 9cc56015d..000000000 --- a/test/unit/handlers/revoke-handler_test.js +++ /dev/null @@ -1,103 +0,0 @@ -'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/revoke-handlers.spec.ts b/test/unit/handlers/revoke-handlers.spec.ts new file mode 100644 index 000000000..70fc355ea --- /dev/null +++ b/test/unit/handlers/revoke-handlers.spec.ts @@ -0,0 +1,126 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { RevokeHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; + +/** + * Test `RevokeHandler`. + */ + +describe('RevokeHandler', () => { + describe('handleRevokeToken()', () => { + it('should call `model.getAccessToken()` and `model.getRefreshToken()`', () => { + const model = { + getClient() {}, + revokeToken: sinon.stub().returns(true), + getRefreshToken: sinon.stub().returns({ + refreshToken: 'hash', + client: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }), + getAccessToken: sinon.stub().returns(false), + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { token: 'foo' }, + headers: {}, + method: 'ANY', + query: {}, + }); + const client = {}; + + return handler + .handleRevokeToken(request, client) + .then(() => { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args[0].should.equal('foo'); + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args[0].should.equal('foo'); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', () => { + it('should call `model.getClient()`', () => { + const model = { + getClient: sinon.stub().returns({ grants: ['password'] }), + revokeToken() {}, + getRefreshToken() {}, + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal('secret'); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', () => { + it('should call `model.getRefreshToken()`', () => { + const model = { + getClient() {}, + revokeToken() {}, + getAccessToken() {}, + getRefreshToken: sinon.stub().returns({ + refreshToken: 'hash', + client: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }), + }; + const handler: any = new RevokeHandler({ model }); + const token = 'hash'; + const client = {}; + + return handler + .getRefreshToken(token, client) + .then(() => { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('revokeToken()', () => { + it('should call `model.revokeToken()`', () => { + const model = { + getClient() {}, + revokeToken: sinon.stub().returns(true), + getRefreshToken: sinon.stub().returns({ + refreshToken: 'hash', + client: {}, + refreshTokenExpiresAt: new Date(Date.now() * 2), + user: {}, + }), + getAccessToken() {}, + }; + const handler: any = new RevokeHandler({ model }); + const token = 'hash'; + + return handler + .revokeToken(token) + .then(() => { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/handlers/token-handler.spec.ts b/test/unit/handlers/token-handler.spec.ts new file mode 100755 index 000000000..53fcd36cf --- /dev/null +++ b/test/unit/handlers/token-handler.spec.ts @@ -0,0 +1,45 @@ +import * as should from 'should'; +import * as sinon from 'sinon'; +import { TokenHandler } from '../../../lib/handlers'; +import { Request } from '../../../lib/request'; + +/** + * Test `TokenHandler`. + */ + +describe('TokenHandler', () => { + describe('getClient()', () => { + it('should call `model.getClient()`', () => { + const model = { + getClient: sinon + .stub() + .returns(Promise.resolve({ grants: ['password'] })), + saveToken() {}, + }; + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: 'ANY', + query: {}, + }); + + return handler + .getClient(request, {}) + .then(() => { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal('secret'); + model.getClient.firstCall.thisValue.should.equal(model); + }) + .catch(() => { + should.fail('should.fail', ''); + }); + }); + }); +}); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js deleted file mode 100644 index 1a0f7c735..000000000 --- a/test/unit/handlers/token-handler_test.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -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'); - -/** - * Test `TokenHandler`. - */ - -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 = { - getClient: sinon.stub().returns({ grants: ['password'] }), - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.args[1].should.equal('secret'); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); -}); diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model.spec.ts old mode 100644 new mode 100755 similarity index 54% rename from test/unit/models/token-model_test.js rename to test/unit/models/token-model.spec.ts index 5aad98ca8..b59f488c9 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model.spec.ts @@ -1,23 +1,23 @@ -var TokenModel = require('../../../lib/models/token-model'); +import { TokenModel } from '../../../lib/models'; /** * Test `Server`. */ -describe('Model', function() { - describe('constructor()', function() { - it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', function() { - var atExpiresAt = new Date(); +describe('Model', () => { + describe('constructor()', () => { + it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', () => { + const atExpiresAt = new Date(); atExpiresAt.setHours(new Date().getHours() + 1); - - var data = { + + const data = { accessToken: 'foo', client: 'bar', user: 'tar', - accessTokenExpiresAt: atExpiresAt + accessTokenExpiresAt: atExpiresAt, }; - - var model = new TokenModel(data); + + const model = new TokenModel(data); model.accessTokenLifetime.should.be.Number(); model.accessTokenLifetime.should.be.approximately(3600, 2); }); diff --git a/test/unit/request.spec.ts b/test/unit/request.spec.ts new file mode 100755 index 000000000..16635b1c3 --- /dev/null +++ b/test/unit/request.spec.ts @@ -0,0 +1,170 @@ +import * as should from 'should'; +import { Request } from '../../lib/request'; + +/** + * Test `Request`. + */ + +function generateBaseRequest() { + return { + query: { + foo: 'bar', + } as any, + method: 'GET', + headers: { + bar: 'foo', + } as any, + body: { + foobar: 'barfoo', + } as any, + } as any; +} + +describe('Request', () => { + it('should instantiate with a basic request', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + }); + + it('should allow a request to be passed without a body', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.body; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql({}); + }); + + it('should throw if headers are not passed to the constructor', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.headers; + + (() => { + new Request(originalRequest); + }).should.throw('Missing parameter: `headers`'); + }); + + it('should throw if query string isnt passed to the constructor', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.query; + + (() => { + new Request(originalRequest); + }).should.throw('Missing parameter: `query`'); + }); + + it('should throw if method isnt passed to the constructor', () => { + const originalRequest = generateBaseRequest(); + delete originalRequest.method; + + (() => { + new Request(originalRequest); + }).should.throw('Missing parameter: `method`'); + }); + + it('should convert all header keys to lowercase', () => { + const originalRequest = generateBaseRequest(); + originalRequest.headers = { + Foo: 'bar', + BAR: 'foo', + } as any; + + const request = new Request(originalRequest); + request.headers.foo.should.eql('bar'); + request.headers.bar.should.eql('foo'); + should.not.exist(request.headers.Foo); + should.not.exist(request.headers.BAR); + }); + + it('should include additional properties passed in the request', () => { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: 'newBar', + }; + + originalRequest.custom2 = { + newBar: 'newFoo', + }; + + const request: any = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it('should include additional properties passed in the request', () => { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: 'newBar', + }; + + originalRequest.custom2 = { + newBar: 'newFoo', + }; + + const request: any = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it('should allow getting of headers using `request.get`', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should allow getting of headers using `request.get`', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should allow getting of headers using `request.get`', () => { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should validate the content-type', () => { + const originalRequest = generateBaseRequest(); + originalRequest.headers['content-type'] = + 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request + .is('application/x-www-form-urlencoded') + .should.eql('application/x-www-form-urlencoded'); + }); + + it('should return false if the content-type is invalid', () => { + const originalRequest = generateBaseRequest(); + originalRequest.headers['content-type'] = + 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request.is('application/json').should.be.false(); + }); +}); diff --git a/test/unit/request_test.js b/test/unit/request_test.js deleted file mode 100644 index 458cb8f9e..000000000 --- a/test/unit/request_test.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Request = require('../../lib/request'); -var should = require('should'); - -/** - * Test `Request`. - */ - -function generateBaseRequest() { - return { - query: { - foo: 'bar' - }, - method: 'GET', - headers: { - bar: 'foo' - }, - body: { - foobar: 'barfoo' - } - }; -} - -describe('Request', function() { - it('should instantiate with a basic request', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - }); - - it('should allow a request to be passed without a body', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.body; - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql({}); - }); - - it('should throw if headers are not passed to the constructor', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.headers; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `headers`'); - }); - - it('should throw if query string isn\'t passed to the constructor', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.query; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `query`'); - }); - - it('should throw if method isn\'t passed to the constructor', function() { - var originalRequest = generateBaseRequest(); - delete originalRequest.method; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `method`'); - }); - - it('should convert all header keys to lowercase', function() { - var originalRequest = generateBaseRequest(); - originalRequest.headers = { - Foo: 'bar', - BAR: 'foo' - }; - - var request = new Request(originalRequest); - request.headers.foo.should.eql('bar'); - request.headers.bar.should.eql('foo'); - should.not.exist(request.headers.Foo); - should.not.exist(request.headers.BAR); - }); - - it('should include additional properties passed in the request', function() { - var originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: 'newBar' - }; - - originalRequest.custom2 = { - newBar: 'newFoo' - }; - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it('should include additional properties passed in the request', function() { - var originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: 'newBar' - }; - - originalRequest.custom2 = { - newBar: 'newFoo' - }; - - var request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it('should allow getting of headers using `request.get`', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should allow getting of headers using `request.get`', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should allow getting of headers using `request.get`', function() { - var originalRequest = generateBaseRequest(); - - var request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should validate the content-type', function() { - var originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; - - var request = new Request(originalRequest); - request.is('application/x-www-form-urlencoded').should.eql('application/x-www-form-urlencoded'); - }); - - it('should return false if the content-type is invalid', function() { - var originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; - - var request = new Request(originalRequest); - request.is('application/json').should.eql(false); - }); -}); diff --git a/test/unit/response.spec.ts b/test/unit/response.spec.ts new file mode 100755 index 000000000..2a2c3f9a9 --- /dev/null +++ b/test/unit/response.spec.ts @@ -0,0 +1,114 @@ +import * as should from 'should'; +import { Response } from '../../lib/response'; + +/** + * Test `Request`. + */ + +const generateBaseResponse = () => { + return { + headers: { + bar: 'foo', + } as any, + body: { + foobar: 'barfoo', + } as any, + } as any; +}; + +describe('Response', () => { + it('should instantiate with a basic request', () => { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it('should allow a response to be passed without a body', () => { + const originalResponse = generateBaseResponse(); + delete originalResponse.body; + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql({}); + response.status.should.eql(200); + }); + + it('should allow a response to be passed without headers', () => { + const originalResponse = generateBaseResponse(); + delete originalResponse.headers; + + const response = new Response(originalResponse); + response.headers.should.eql({}); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it('should convert all header keys to lowercase', () => { + const originalResponse = generateBaseResponse(); + originalResponse.headers = { + Foo: 'bar', + BAR: 'foo', + }; + + const response: any = new Response(originalResponse); + response.headers.foo.should.eql('bar'); + response.headers.bar.should.eql('foo'); + should.not.exist(response.headers.Foo); + should.not.exist(response.headers.BAR); + }); + + it('should include additional properties passed in the response', () => { + const originalResponse = generateBaseResponse(); + originalResponse.custom = { + newFoo: 'newBar', + }; + + originalResponse.custom2 = { + newBar: 'newFoo', + }; + + const response: any = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.custom.should.eql(originalResponse.custom); + response.custom2.should.eql(originalResponse.custom2); + }); + + it('should allow getting of headers using `response.get`', () => { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get('bar').should.eql(originalResponse.headers.bar); + }); + + it('should allow getting of headers using `response.get`', () => { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get('bar').should.eql(originalResponse.headers.bar); + }); + + it('should allow setting of headers using `response.set`', () => { + const originalResponse = generateBaseResponse(); + + const response: any = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.set('new_header', 'new_value'); + response.headers.bar.should.eql('foo'); + response.headers.new_header.should.eql('new_value'); + }); + + it('should process redirect', () => { + const originalResponse = generateBaseResponse(); + + const response: any = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.status.should.eql(200); + response.redirect('http://foo.bar'); + response.headers.location.should.eql('http://foo.bar'); + response.status.should.eql(302); + }); +}); diff --git a/test/unit/response_test.js b/test/unit/response_test.js deleted file mode 100644 index d226df673..000000000 --- a/test/unit/response_test.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var Response = require('../../lib/response'); -var should = require('should'); - -/** - * Test `Request`. - */ - -function generateBaseResponse() { - return { - headers: { - bar: 'foo' - }, - body: { - foobar: 'barfoo' - } - }; -} - -describe('Request', function() { - it('should instantiate with a basic request', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it('should allow a response to be passed without a body', function() { - var originalResponse = generateBaseResponse(); - delete originalResponse.body; - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql({}); - response.status.should.eql(200); - }); - - it('should allow a response to be passed without headers', function() { - var originalResponse = generateBaseResponse(); - delete originalResponse.headers; - - var response = new Response(originalResponse); - response.headers.should.eql({}); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it('should convert all header keys to lowercase', function() { - var originalResponse = generateBaseResponse(); - originalResponse.headers = { - Foo: 'bar', - BAR: 'foo' - }; - - var response = new Response(originalResponse); - response.headers.foo.should.eql('bar'); - response.headers.bar.should.eql('foo'); - should.not.exist(response.headers.Foo); - should.not.exist(response.headers.BAR); - }); - - it('should include additional properties passed in the response', function() { - var originalResponse = generateBaseResponse(); - originalResponse.custom = { - newFoo: 'newBar' - }; - - originalResponse.custom2 = { - newBar: 'newFoo' - }; - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.custom.should.eql(originalResponse.custom); - response.custom2.should.eql(originalResponse.custom2); - }); - - it('should allow getting of headers using `response.get`', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.get('bar').should.eql(originalResponse.headers.bar); - }); - - it('should allow getting of headers using `response.get`', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.get('bar').should.eql(originalResponse.headers.bar); - }); - - it('should allow setting of headers using `response.set`', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.set('newheader', 'newvalue'); - response.headers.bar.should.eql('foo'); - response.headers.newheader.should.eql('newvalue'); - }); - - it('should process redirect', function() { - var originalResponse = generateBaseResponse(); - - var response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.status.should.eql(200); - response.redirect('http://foo.bar'); - response.headers.location.should.eql('http://foo.bar'); - response.status.should.eql(302); - }); -}); diff --git a/test/unit/server.spec.ts b/test/unit/server.spec.ts new file mode 100755 index 000000000..2ce3b4f7d --- /dev/null +++ b/test/unit/server.spec.ts @@ -0,0 +1,91 @@ +import * as sinon from 'sinon'; +import { + AuthenticateHandler, + AuthorizeHandler, + TokenHandler, +} from '../../lib/handlers'; +import { OAuth2Server as Server } from '../../lib/server'; + +const Authenticate: any = AuthenticateHandler; +const Authorize: any = AuthorizeHandler; +const Token: any = TokenHandler; +/** + * Test `Server`. + */ + +describe('Server', () => { + describe('authenticate()', () => { + it('should call `handle`', async () => { + const model = { + getAccessToken() {}, + }; + const server = new Server({ model }); + + sinon.stub(Authenticate.prototype, 'handle').returns(Promise.resolve()); + + await server.authenticate('foo' as any); + + Authenticate.prototype.handle.callCount.should.equal(1); + Authenticate.prototype.handle.firstCall.args[0].should.equal('foo'); + Authenticate.prototype.handle.restore(); + }); + + it('should map string passed as `options` to `options.scope`', async () => { + const model = { + getAccessToken() {}, + verifyScope() {}, + }; + const server = new Server({ model }); + + sinon.stub(Authenticate.prototype, 'handle').returns(Promise.resolve()); + + await server.authenticate('foo' as any, 'bar' as any, 'test'); + + Authenticate.prototype.handle.callCount.should.equal(1); + Authenticate.prototype.handle.firstCall.args[0].should.equal('foo'); + Authenticate.prototype.handle.firstCall.args[1].should.equal('bar'); + Authenticate.prototype.handle.firstCall.thisValue.should.have.property( + 'scope', + 'test', + ); + Authenticate.prototype.handle.restore(); + }); + }); + + describe('authorize()', () => { + it('should call `handle`', async () => { + const model = { + getAccessToken() {}, + getClient() {}, + saveAuthorizationCode() {}, + }; + const server = new Server({ model }); + + sinon.stub(Authorize.prototype, 'handle').returns(Promise.resolve()); + + await server.authorize('foo' as any, 'bar' as any); + + Authorize.prototype.handle.callCount.should.equal(1); + Authorize.prototype.handle.firstCall.args[0].should.equal('foo'); + Authorize.prototype.handle.restore(); + }); + }); + + describe('token()', () => { + it('should call `handle`', async () => { + const model = { + getClient() {}, + saveToken() {}, + }; + const server = new Server({ model }); + + sinon.stub(Token.prototype, 'handle').returns(Promise.resolve()); + + await server.token('foo' as any, 'bar' as any); + + Token.prototype.handle.callCount.should.equal(1); + Token.prototype.handle.firstCall.args[0].should.equal('foo'); + Token.prototype.handle.restore(); + }); + }); +}); diff --git a/test/unit/server_test.js b/test/unit/server_test.js deleted file mode 100644 index 21083bef6..000000000 --- a/test/unit/server_test.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var AuthenticateHandler = require('../../lib/handlers/authenticate-handler'); -var AuthorizeHandler = require('../../lib/handlers/authorize-handler'); -var Promise = require('bluebird'); -var Server = require('../../lib/server'); -var TokenHandler = require('../../lib/handlers/token-handler'); -var RevokeHandler = require('../../lib/handlers/revoke-handler'); -var sinon = require('sinon'); - -/** - * Test `Server`. - */ - -describe('Server', function() { - describe('authenticate()', function() { - it('should call `handle`', function() { - var model = { - getAccessToken: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); - - server.authenticate('foo'); - - AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthenticateHandler.prototype.handle.restore(); - }); - - it('should map string passed as `options` to `options.scope`', function() { - var model = { - getAccessToken: function() {}, - verifyScope: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); - - server.authenticate('foo', 'bar', 'test'); - - AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthenticateHandler.prototype.handle.firstCall.args[1].should.equal('bar'); - AuthenticateHandler.prototype.handle.firstCall.thisValue.should.have.property('scope', 'test'); - AuthenticateHandler.prototype.handle.restore(); - }); - }); - - describe('authorize()', function() { - it('should call `handle`', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(AuthorizeHandler.prototype, 'handle').returns(Promise.resolve()); - - server.authorize('foo', 'bar'); - - AuthorizeHandler.prototype.handle.callCount.should.equal(1); - AuthorizeHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthorizeHandler.prototype.handle.restore(); - }); - }); - - describe('token()', function() { - it('should call `handle`', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var server = new Server({ model: model }); - - sinon.stub(TokenHandler.prototype, 'handle').returns(Promise.resolve()); - - server.token('foo', 'bar'); - - TokenHandler.prototype.handle.callCount.should.equal(1); - TokenHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - TokenHandler.prototype.handle.restore(); - }); - }); - - 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(); - }); - }); -}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100755 index 000000000..56ee33060 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": true + }, + "exclude": ["node_modules", "test", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 000000000..b42af265a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "target": "es2017", + "sourceMap": true, + "importHelpers": true, + "outDir": "./dist", + "baseUrl": ".", + "typeRoots": ["node_modules/@types"], + "downlevelIteration": false, + "emitDecoratorMetadata": false, + "experimentalDecorators": false, + "moduleResolution": "node", + "lib": ["es2018"] + }, + "exclude": ["node_modules", "./dist"] +} diff --git a/tslint.json b/tslint.json new file mode 100755 index 000000000..db341dfc4 --- /dev/null +++ b/tslint.json @@ -0,0 +1,28 @@ +{ + "extends": ["tslint:recommended", "tslint:all", "tslint:latest"], + "jsRules": { + "no-unused-expression": true + }, + "rules": { + "arrow-parens": false, + "completed-docs": false, + "file-name-casing": [true, "kebab-case"], + "no-object-literal-type-assertion": [false], + "interface-name": [false], + "max-line-length": [true, 120], + "member-access": [false], + "no-boolean-literal-compare": [false], + "member-ordering": [false], + "no-default-export": [false], + "prefer-function-over-method": [false], + "no-magic-numbers": [false], + "newline-per-chained-call": false, + "no-unsafe-any": false, + "object-literal-sort-keys": false, + "ordered-imports": [false], + "quotemark": [true, "single"], + "semicolon": [true, "always", "ignore-bound-class-methods"], + "strict-boolean-expressions": [false] + }, + "rulesDirectory": [] +}