From 4f7899b798c16cec0aee08285012ffb5f6fd3c4b Mon Sep 17 00:00:00 2001 From: Thom Seddon Date: Wed, 11 Mar 2015 15:45:43 +0000 Subject: [PATCH 01/39] 2.4.0 --- Changelog.md | 17 +++++++++++++++++ package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 54d34410e..dfb6c6f35 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,22 @@ ## Changelog +### 2.4.0 + +- Set Cache-Control and Pragma headers +- Allow any valid URI for extension grants +- Expose `client` to `extendedGrant` and after via `req.oauth.client` +- Fix express depreciation warning for `res.send()` +- Expose `user` to `generateToken` and after via `req.user` +- Fix lockdown pattern for express 3 + +- Add redis example +- Fix docs to use new express bodyParser module +- Fix docs for `redirect_uri` +- Clarify docs for `clientIdRegex` +- Fix docs for missing `req` argument in `generateToken` +- Fix docs for `user`/`userId` `getAccessToken` +- Fix docs for argument order in `getRefreshToken` + ### 2.3.0 - Support "state" param for auth_code grant type diff --git a/package.json b/package.json index f82742fe1..ca79c8a64 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oauth2-server", "description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with express in node.js", - "version": "2.3.0", + "version": "2.4.0", "keywords": [ "oauth", "oauth2" From 7d7fe349f586f5829ee86c1b2b30057db2e40863 Mon Sep 17 00:00:00 2001 From: John Wehr Date: Mon, 27 Apr 2015 19:25:49 -0400 Subject: [PATCH 02/39] Fix for https://github.com/thomseddon/koa-oauth-server/issues/29 --- lib/grant.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/grant.js b/lib/grant.js index 1064fa204..760d27c0e 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -466,10 +466,8 @@ function sendResponse (done) { if (this.refreshToken) response.refresh_token = this.refreshToken; - this.res - .set('Cache-Control', 'no-store') - .set('Pragma', 'no-cache') - .jsonp(response); + this.res.set({'Cache-Control':'no-store', 'Pragma': 'no-cache'}); + this.res.jsonp(response); if (this.config.continueAfterResponse) done(); From 213a8436f536b257b75f1aeca8bd7ff8a0064100 Mon Sep 17 00:00:00 2001 From: Shane Niu Date: Thu, 7 May 2015 17:57:21 +1000 Subject: [PATCH 03/39] update grant types in README. --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 6d4b8654f..1a8d8d912 100644 --- a/Readme.md +++ b/Readme.md @@ -55,7 +55,7 @@ Note: As no model was actually implemented here, delving any deeper, i.e. passin - *string* **model** - Model object (see below) - *array* **grants** - - grant types you wish to support, currently the module supports `password` and `refresh_token` + - grant types you wish to support, currently the module supports `authorization_code`, `password`, `refresh_token` and `client_credentials` - Default: `[]` - *function|boolean* **debug** - If `true` errors will be logged to console. You may also pass a custom function, in which case that function will be called with the error as its first argument From 1f5921f25b9debf860485dfb2f2a626509e16055 Mon Sep 17 00:00:00 2001 From: John Wehr Date: Tue, 12 May 2015 00:57:40 -0400 Subject: [PATCH 04/39] Whitespace formatting. --- lib/grant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grant.js b/lib/grant.js index 760d27c0e..dc49681eb 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -466,7 +466,7 @@ function sendResponse (done) { if (this.refreshToken) response.refresh_token = this.refreshToken; - this.res.set({'Cache-Control':'no-store', 'Pragma': 'no-cache'}); + this.res.set({'Cache-Control': 'no-store', 'Pragma': 'no-cache'}); this.res.jsonp(response); if (this.config.continueAfterResponse) From b36a06b445ad0a676e6175d68a8bd0b2f3353dbf Mon Sep 17 00:00:00 2001 From: Thom Seddon Date: Mon, 29 Jun 2015 10:39:40 +0100 Subject: [PATCH 05/39] 2.4.1 --- Changelog.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index dfb6c6f35..dbd25a6a4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,10 @@ ## Changelog +### 2.4.1 + +- Fix header setting syntax +- Fix docs for supported grant types + ### 2.4.0 - Set Cache-Control and Pragma headers diff --git a/package.json b/package.json index ca79c8a64..2fe70df8d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oauth2-server", "description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with express in node.js", - "version": "2.4.0", + "version": "2.4.1", "keywords": [ "oauth", "oauth2" From 32ff927bbf54f7317ec5b06100028ccb5abc0f5a Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 17 Mar 2015 23:34:58 +0000 Subject: [PATCH 06/39] Refactor authenticate, authorise and token grants --- .jshintrc | 29 + LICENSE | 202 ---- Readme.md | 4 +- index.js | 8 + lib/authCodeGrant.js | 196 ---- lib/authorise.js | 130 --- lib/error.js | 70 -- lib/errors/access-denied-error.js | 31 + lib/errors/invalid-argument-error.js | 29 + lib/errors/invalid-client-error.js | 31 + lib/errors/invalid-grant-error.js | 31 + lib/errors/invalid-request-error.js | 31 + lib/errors/invalid-token-error.js | 31 + lib/errors/oauth-error.js | 27 + lib/errors/server-error.js | 31 + lib/errors/unsupported-grant-type-error.js | 31 + .../authorization-code-grant-type.js | 77 ++ .../client-credentials-grant-type.js | 56 ++ lib/grant-types/password-grant-type.js | 61 ++ lib/grant-types/refresh-token-grant-type.js | 77 ++ lib/grant.js | 474 ---------- lib/handlers/authenticate-handler.js | 145 +++ lib/handlers/authorize-handler.js | 256 +++++ lib/handlers/token-handler.js | 299 ++++++ lib/oauth2server.js | 232 ----- lib/request.js | 58 ++ lib/response-types/code-response-type.js | 42 + lib/response-types/token-response-type.js | 14 + lib/response.js | 35 + lib/runner.js | 20 - lib/server.js | 64 ++ lib/token-types/bearer-token-type.js | 51 + lib/token-types/mac-token-type.js | 14 + lib/token.js | 57 -- lib/utils/token-util.js | 28 + package.json | 27 +- test/assertions.js | 16 + test/authCodeGrant.js | 385 -------- test/authorise.js | 240 ----- test/error.js | 81 -- test/errorHandler.js | 89 -- test/grant.authorization_code.js | 228 ----- test/grant.client_credentials.js | 73 -- test/grant.extended.js | 181 ---- test/grant.js | 561 ----------- test/grant.password.js | 98 -- test/grant.refresh_token.js | 286 ------ .../authorization-code-grant-type_test.js | 231 +++++ .../client-credentials-grant-type_test.js | 137 +++ .../grant-types/password-grant-type_test.js | 146 +++ .../refresh-token-grant-type_test.js | 206 ++++ .../handlers/authenticate-handler_test.js | 308 ++++++ .../handlers/authorize-handler_test.js | 713 ++++++++++++++ .../handlers/token-handler_test.js | 878 ++++++++++++++++++ test/integration/request_test.js | 158 ++++ .../response-types/code-response-type_test.js | 63 ++ test/integration/response_test.js | 60 ++ test/integration/server_test.js | 164 ++++ .../token-types/bearer-token-type_test.js | 93 ++ test/integration/utils/token-util_test.js | 20 + test/lockdown.js | 134 --- test/mocha.opts | 4 + .../handlers/authenticate-handler_test.js | 91 ++ test/unit/handlers/authorize-handler_test.js | 68 ++ test/unit/handlers/token-handler_test.js | 82 ++ test/unit/server_test.js | 71 ++ 66 files changed, 5110 insertions(+), 3754 deletions(-) create mode 100644 .jshintrc delete mode 100644 LICENSE create mode 100644 index.js delete mode 100644 lib/authCodeGrant.js delete mode 100644 lib/authorise.js delete mode 100644 lib/error.js create mode 100644 lib/errors/access-denied-error.js create mode 100644 lib/errors/invalid-argument-error.js create mode 100644 lib/errors/invalid-client-error.js create mode 100644 lib/errors/invalid-grant-error.js create mode 100644 lib/errors/invalid-request-error.js create mode 100644 lib/errors/invalid-token-error.js create mode 100644 lib/errors/oauth-error.js create mode 100644 lib/errors/server-error.js create mode 100644 lib/errors/unsupported-grant-type-error.js create mode 100644 lib/grant-types/authorization-code-grant-type.js create mode 100644 lib/grant-types/client-credentials-grant-type.js create mode 100644 lib/grant-types/password-grant-type.js create mode 100644 lib/grant-types/refresh-token-grant-type.js delete mode 100644 lib/grant.js create mode 100644 lib/handlers/authenticate-handler.js create mode 100644 lib/handlers/authorize-handler.js create mode 100644 lib/handlers/token-handler.js delete mode 100644 lib/oauth2server.js create mode 100644 lib/request.js create mode 100644 lib/response-types/code-response-type.js create mode 100644 lib/response-types/token-response-type.js create mode 100644 lib/response.js delete mode 100644 lib/runner.js create mode 100644 lib/server.js create mode 100644 lib/token-types/bearer-token-type.js create mode 100644 lib/token-types/mac-token-type.js delete mode 100644 lib/token.js create mode 100644 lib/utils/token-util.js create mode 100644 test/assertions.js delete mode 100644 test/authCodeGrant.js delete mode 100644 test/authorise.js delete mode 100644 test/error.js delete mode 100644 test/errorHandler.js delete mode 100644 test/grant.authorization_code.js delete mode 100644 test/grant.client_credentials.js delete mode 100644 test/grant.extended.js delete mode 100644 test/grant.js delete mode 100644 test/grant.password.js delete mode 100644 test/grant.refresh_token.js create mode 100644 test/integration/grant-types/authorization-code-grant-type_test.js create mode 100644 test/integration/grant-types/client-credentials-grant-type_test.js create mode 100644 test/integration/grant-types/password-grant-type_test.js create mode 100644 test/integration/grant-types/refresh-token-grant-type_test.js create mode 100644 test/integration/handlers/authenticate-handler_test.js create mode 100644 test/integration/handlers/authorize-handler_test.js create mode 100644 test/integration/handlers/token-handler_test.js create mode 100644 test/integration/request_test.js create mode 100644 test/integration/response-types/code-response-type_test.js create mode 100644 test/integration/response_test.js create mode 100644 test/integration/server_test.js create mode 100644 test/integration/token-types/bearer-token-type_test.js create mode 100644 test/integration/utils/token-util_test.js delete mode 100644 test/lockdown.js create mode 100644 test/mocha.opts create mode 100644 test/unit/handlers/authenticate-handler_test.js create mode 100644 test/unit/handlers/authorize-handler_test.js create mode 100644 test/unit/handlers/token-handler_test.js create mode 100644 test/unit/server_test.js diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..0cb361ad3 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,29 @@ +{ + "bitwise": true, + "browser": true, + "curly": true, + "eqeqeq": true, + "esnext": true, + "expr": true, + "globalstrict": false, + "globals": { + "Promise": true + }, + "immed": true, + "indent": 2, + "jquery": true, + "latedef": false, + "mocha": true, + "newcap": true, + "noarg": true, + "node": true, + "noyield": true, + "quotmark": "single", + "regexp": true, + "smarttabs": true, + "strict": false, + "trailing": false, + "undef": true, + "unused": true, + "white": false +} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7a4a3ea24..000000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/Readme.md b/Readme.md index 1a8d8d912..701f8f2b7 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# Node OAuth2 Server [![Build Status](https://travis-ci.org/thomseddon/node-oauth2-server.png?branch=2.0)](https://travis-ci.org/thomseddon/node-oauth2-server) +# Node OAuth2 Server [![Build Status](https://travis-ci.org/thomseddon/node-oauth2-server.png)](https://travis-ci.org/thomseddon/node-oauth2-server) Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with [express](http://expressjs.com/) in [node.js](http://nodejs.org/) @@ -72,7 +72,7 @@ Note: As no model was actually implemented here, delving any deeper, i.e. passin - Life of auth codes in seconds - Default: `30` - *regexp* **clientIdRegex** - - Regex to sanity check client id against before checking model. Note: the default just matches common `client_id` structures, change as needed + - Regex to sanity check client id against before checking model. Note: the default just matches common `client_id` structures, change as needed - Default: `/^[a-z0-9-_]{3,40}$/i` - *boolean* **passthroughErrors** - If true, **non grant** errors will not be handled internally (so you can ensure a consistent format with the rest of your api) diff --git a/index.js b/index.js new file mode 100644 index 000000000..7f93bdaa6 --- /dev/null +++ b/index.js @@ -0,0 +1,8 @@ + +/** + * Expose server and request/response classes. + */ + +module.exports = require('./lib/server'); +module.exports.Request = require('./lib/request'); +module.exports.Response = require('./lib/response'); diff --git a/lib/authCodeGrant.js b/lib/authCodeGrant.js deleted file mode 100644 index 616bfbdeb..000000000 --- a/lib/authCodeGrant.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var error = require('./error'), - runner = require('./runner'), - token = require('./token'); - -module.exports = AuthCodeGrant; - -/** - * This is the function order used by the runner - * - * @type {Array} - */ -var fns = [ - checkParams, - checkClient, - checkUserApproved, - generateCode, - saveAuthCode, - redirect -]; - -/** - * AuthCodeGrant - * - * @param {Object} config Instance of OAuth object - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -function AuthCodeGrant(config, req, res, next, check) { - this.config = config; - this.model = config.model; - this.req = req; - this.res = res; - this.check = check; - - var self = this; - runner(fns, this, function (err) { - if (err && res.oauthRedirect) { - // Custom redirect error handler - res.redirect(self.client.redirectUri + '?error=' + err.error + - '&error_description=' + err.error_description + '&code=' + err.code); - - return self.config.continueAfterResponse ? next() : null; - } - - next(err); - }); -} - -/** - * Check Request Params - * - * @param {Function} done - * @this OAuth - */ -function checkParams (done) { - var body = this.req.body; - var query = this.req.query; - if (!body && !query) return done(error('invalid_request')); - - // Response type - this.responseType = body.response_type || query.response_type; - if (this.responseType !== 'code') { - return done(error('invalid_request', - 'Invalid response_type parameter (must be "code")')); - } - - // Client - this.clientId = body.client_id || query.client_id; - if (!this.clientId) { - return done(error('invalid_request', - 'Invalid or missing client_id parameter')); - } - - // Redirect URI - this.redirectUri = body.redirect_uri || query.redirect_uri; - if (!this.redirectUri) { - return done(error('invalid_request', - 'Invalid or missing redirect_uri parameter')); - } - - done(); -} - -/** - * Check client against model - * - * @param {Function} done - * @this OAuth - */ -function checkClient (done) { - var self = this; - this.model.getClient(this.clientId, null, function (err, client) { - if (err) return done(error('server_error', false, err)); - - if (!client) { - return done(error('invalid_client', 'Invalid client credentials')); - } else if (Array.isArray(client.redirectUri)) { - if (client.redirectUri.indexOf(self.redirectUri) === -1) { - return done(error('invalid_request', 'redirect_uri does not match')); - } - client.redirectUri = self.redirectUri; - } else if (client.redirectUri !== self.redirectUri) { - return done(error('invalid_request', 'redirect_uri does not match')); - } - - // The request contains valid params so any errors after this point - // are redirected to the redirect_uri - self.res.oauthRedirect = true; - self.client = client; - - done(); - }); -} - -/** - * Check client against model - * - * @param {Function} done - * @this OAuth - */ -function checkUserApproved (done) { - var self = this; - this.check(this.req, function (err, allowed, user) { - if (err) return done(error('server_error', false, err)); - - if (!allowed) { - return done(error('access_denied', - 'The user denied access to your application')); - } - - self.user = user; - done(); - }); -} - -/** - * Check client against model - * - * @param {Function} done - * @this OAuth - */ -function generateCode (done) { - var self = this; - token(this, 'authorization_code', function (err, code) { - self.authCode = code; - done(err); - }); -} - -/** - * Check client against model - * - * @param {Function} done - * @this OAuth - */ -function saveAuthCode (done) { - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + this.config.authCodeLifetime); - - this.model.saveAuthCode(this.authCode, this.client.clientId, expires, - this.user, function (err) { - if (err) return done(error('server_error', false, err)); - done(); - }); -} - -/** - * Check client against model - * - * @param {Function} done - * @this OAuth - */ -function redirect (done) { - this.res.redirect(this.client.redirectUri + '?code=' + this.authCode + - (this.req.query.state ? '&state=' + this.req.query.state : '')); - - if (this.config.continueAfterResponse) - return done(); -} diff --git a/lib/authorise.js b/lib/authorise.js deleted file mode 100644 index 2b8296019..000000000 --- a/lib/authorise.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var error = require('./error'), - runner = require('./runner'); - -module.exports = Authorise; - -/** - * This is the function order used by the runner - * - * @type {Array} - */ -var fns = [ - getBearerToken, - checkToken -]; - -/** - * Authorise - * - * @param {Object} config Instance of OAuth object - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -function Authorise (config, req, next) { - this.config = config; - this.model = config.model; - this.req = req; - - runner(fns, this, next); -} - -/** - * Get bearer token - * - * Extract token from request according to RFC6750 - * - * @param {Function} done - * @this OAuth - */ -function getBearerToken (done) { - var headerToken = this.req.get('Authorization'), - getToken = this.req.query.access_token, - postToken = this.req.body ? this.req.body.access_token : undefined; - - // Check exactly one method was used - var methodsUsed = (headerToken !== undefined) + (getToken !== undefined) + - (postToken !== undefined); - - if (methodsUsed > 1) { - return done(error('invalid_request', - 'Only one method may be used to authenticate at a time (Auth header, ' + - 'GET or POST).')); - } else if (methodsUsed === 0) { - return done(error('invalid_request', 'The access token was not found')); - } - - // Header: http://tools.ietf.org/html/rfc6750#section-2.1 - if (headerToken) { - var matches = headerToken.match(/Bearer\s(\S+)/); - - if (!matches) { - return done(error('invalid_request', 'Malformed auth header')); - } - - headerToken = matches[1]; - } - - // POST: http://tools.ietf.org/html/rfc6750#section-2.2 - if (postToken) { - if (this.req.method === 'GET') { - return done(error('invalid_request', - 'Method cannot be GET When putting the token in the body.')); - } - - if (!this.req.is('application/x-www-form-urlencoded')) { - return done(error('invalid_request', 'When putting the token in the ' + - 'body, content type must be application/x-www-form-urlencoded.')); - } - } - - this.bearerToken = headerToken || postToken || getToken; - done(); -} - -/** - * Check token - * - * Check it against model, ensure it's not expired - * @param {Function} done - * @this OAuth - */ -function checkToken (done) { - var self = this; - this.model.getAccessToken(this.bearerToken, function (err, token) { - if (err) return done(error('server_error', false, err)); - - if (!token) { - return done(error('invalid_token', - 'The access token provided is invalid.')); - } - - if (token.expires !== null && - (!token.expires || token.expires < new Date())) { - return done(error('invalid_token', - 'The access token provided has expired.')); - } - - // Expose params - self.req.oauth = { bearerToken: token }; - self.req.user = token.user ? token.user : { id: token.userId }; - - done(); - }); -} diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index d1971e8af..000000000 --- a/lib/error.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var util = require('util'); - -module.exports = OAuth2Error; - -/** - * Error - * - * @param {Number} code Numeric error code - * @param {String} error Error descripton - * @param {String} description Full error description - */ -function OAuth2Error (error, description, err) { - if (!(this instanceof OAuth2Error)) - return new OAuth2Error(error, description, err); - - Error.call(this); - - this.name = this.constructor.name; - if (err instanceof Error) { - this.message = err.message; - this.stack = err.stack; - } else { - this.message = description; - Error.captureStackTrace(this, this.constructor); - } - - this.headers = { - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache' - }; - - switch (error) { - case 'invalid_client': - this.headers['WWW-Authenticate'] = 'Basic realm="Service"'; - /* falls through */ - case 'invalid_grant': - case 'invalid_request': - this.code = 400; - break; - case 'invalid_token': - this.code = 401; - break; - case 'server_error': - this.code = 503; - break; - default: - this.code = 500; - } - - this.error = error; - this.error_description = description || error; -} - -util.inherits(OAuth2Error, Error); diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js new file mode 100644 index 000000000..e2f6a3dc3 --- /dev/null +++ b/lib/errors/access-denied-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function AccessDeniedError(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'access_denied' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(AccessDeniedError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = AccessDeniedError; diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js new file mode 100644 index 000000000..234ca83ad --- /dev/null +++ b/lib/errors/invalid-argument-error.js @@ -0,0 +1,29 @@ +/** + * Module dependencies. + */ + +var OAuthError = require('standard-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function InvalidArgumentError(message) { + OAuthError.call(this, { + code: 500, + message: message + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(InvalidArgumentError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = InvalidArgumentError; diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js new file mode 100644 index 000000000..189c17fff --- /dev/null +++ b/lib/errors/invalid-client-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function InvalidClientError(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'invalid_client' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(InvalidClientError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = InvalidClientError; diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js new file mode 100644 index 000000000..3b31529ca --- /dev/null +++ b/lib/errors/invalid-grant-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function InvalidGrantError(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'invalid_grant' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(InvalidGrantError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = InvalidGrantError; diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js new file mode 100644 index 000000000..a0417465f --- /dev/null +++ b/lib/errors/invalid-request-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function InvalidRequest(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'invalid_request' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(InvalidRequest, OAuthError); + +/** + * Export constructor. + */ + +module.exports = InvalidRequest; diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js new file mode 100644 index 000000000..e6d435a63 --- /dev/null +++ b/lib/errors/invalid-token-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function InvalidTokenError(message) { + OAuthError.call(this, { + code: 401, + message: message, + name: 'invalid_token' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(InvalidTokenError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = InvalidTokenError; diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js new file mode 100644 index 000000000..0a5178221 --- /dev/null +++ b/lib/errors/oauth-error.js @@ -0,0 +1,27 @@ + +/** + * Module dependencies. + */ + +var StandardError = require("standard-error"); +var util = require('util'); + +/** + * Constructor. + */ + +function OAuthError(message, properties) { + StandardError.call(this, message, properties); +} + +/** + * Inherit prototype. + */ + +util.inherits(OAuthError, StandardError); + +/** + * Export constructor. + */ + +module.exports = OAuthError; diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js new file mode 100644 index 000000000..43fc25104 --- /dev/null +++ b/lib/errors/server-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function ServerError(message) { + OAuthError.call(this, { + code: 503, + message: message, + name: 'server_error' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(ServerError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = ServerError; diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js new file mode 100644 index 000000000..402025fea --- /dev/null +++ b/lib/errors/unsupported-grant-type-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function UnsupportedGrantTypeError(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'unsupported_grant_type' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(UnsupportedGrantTypeError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = UnsupportedGrantTypeError; diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js new file mode 100644 index 000000000..b62fe4ba2 --- /dev/null +++ b/lib/grant-types/authorization-code-grant-type.js @@ -0,0 +1,77 @@ + +/** + * Module dependencies. + */ + +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 ServerError = require('../errors/server-error'); + +/** + * Constructor. + */ + +function AuthCodeGrantType(model) { + if (!model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!model.getAuthCode) { + throw new ServerError('Server error: model does not implement `getAuthCode()`'); + } + + this.model = model; +} + +/** + * Retrieve the user from the model using an authorization code. + * + * (See: https://tools.ietf.org/html/rfc6749#section-4.1.3) + */ + +AuthCodeGrantType.prototype.handle = function(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + if (!request.body.code) { + return Promise.reject(new InvalidRequestError('Missing parameter: `code`')); + } + + return Promise.try(this.model.getAuthCode, request.body.code) + .then(function(authCode) { + if (!authCode) { + throw new InvalidGrantError('Invalid grant: authorization code is invalid'); + } + + if (!authCode.client) { + throw new ServerError('Server error: `getAuthCode()` did not return a `client` object'); + } + + if (!authCode.user) { + throw new ServerError('Server error: `getAuthCode()` did not return a `user` object'); + } + + if (authCode.client.id !== client.id) { + throw new InvalidGrantError('Invalid grant: authorization code is invalid'); + } + + if (authCode.expires && authCode.expires < new Date()) { + throw new InvalidGrantError('Invalid grant: authorization code has expired'); + } + + return authCode; + }); +}; + +/** + * Export constructor. + */ + +module.exports = AuthCodeGrantType; diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js new file mode 100644 index 000000000..0bad54a55 --- /dev/null +++ b/lib/grant-types/client-credentials-grant-type.js @@ -0,0 +1,56 @@ + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var InvalidGrantError = require('../errors/invalid-grant-error'); +var Promise = require('bluebird'); +var ServerError = require('../errors/server-error'); + +/** + * Constructor. + */ + +function ClientCredentialsType(model) { + if (!model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!model.getUserFromClient) { + throw new ServerError('Server error: model does not implement `getUserFromClient()`'); + } + + this.model = model; +} + +/** + * Retrieve the user from the model using client credentials. + * + * (See: https://tools.ietf.org/html/rfc6749#section-4.4.2) + */ + +ClientCredentialsType.prototype.handle = function(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + return Promise.try(this.model.getUserFromClient, client) + .then(function(user) { + if (!user) { + throw new InvalidGrantError('Invalid grant: user credentials are invalid'); + } + + return user; + }); +}; + +/** + * Export constructor. + */ + +module.exports = ClientCredentialsType; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js new file mode 100644 index 000000000..3c89b1c63 --- /dev/null +++ b/lib/grant-types/password-grant-type.js @@ -0,0 +1,61 @@ + +/** + * Module dependencies. + */ + +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 ServerError = require('../errors/server-error'); + +/** + * Constructor. + */ + +function PasswordGrantType(model) { + if (!model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!model.getUser) { + throw new ServerError('Server error: model does not implement `getUser()`'); + } + + this.model = model; +} + +/** + * 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) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!request.body.username) { + return Promise.reject(new InvalidRequestError('Missing parameter: `username`')); + } + + if (!request.body.password) { + return Promise.reject(new InvalidRequestError('Missing parameter: `password`')); + } + + return Promise.try(this.model.getUser, [request.body.username, request.body.password]) + .then(function(user) { + if (!user) { + throw new InvalidGrantError('Invalid grant: user credentials are invalid'); + } + + return user; + }); +}; + +/** + * Export constructor. + */ + +module.exports = PasswordGrantType; diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js new file mode 100644 index 000000000..bf7585602 --- /dev/null +++ b/lib/grant-types/refresh-token-grant-type.js @@ -0,0 +1,77 @@ + +/** + * Module dependencies. + */ + +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 ServerError = require('../errors/server-error'); + +/** + * Constructor. + */ + +function RefreshTokenGrantType(model) { + if (!model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!model.getRefreshToken) { + throw new ServerError('Server error: model does not implement `getRefreshToken()`'); + } + + this.model = model; +} + +/** + * Retrieve the user from the model using a refresh_token. + * + * (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`'); + } + + if (!request.body.refresh_token) { + return Promise.reject(new InvalidRequestError('Missing parameter: `refresh_token`')); + } + + return Promise.try(this.model.getRefreshToken, request.body.refresh_token) + .then(function(refreshToken) { + if (!refreshToken) { + throw new InvalidGrantError('Invalid grant: 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 InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if (refreshToken.expires && refreshToken.expires < new Date()) { + throw new InvalidGrantError('Invalid grant: refresh token has expired'); + } + + return refreshToken; + }); +}; + +/** + * Export constructor. + */ + +module.exports = RefreshTokenGrantType; diff --git a/lib/grant.js b/lib/grant.js deleted file mode 100644 index dc49681eb..000000000 --- a/lib/grant.js +++ /dev/null @@ -1,474 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var auth = require('basic-auth'), - error = require('./error'), - runner = require('./runner'), - token = require('./token'); - -module.exports = Grant; - -/** - * This is the function order used by the runner - * - * @type {Array} - */ -var fns = [ - extractCredentials, - checkClient, - checkGrantTypeAllowed, - checkGrantType, - exposeUser, - generateAccessToken, - saveAccessToken, - generateRefreshToken, - saveRefreshToken, - sendResponse -]; - -/** - * Grant - * - * @param {Object} config Instance of OAuth object - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -function Grant (config, req, res, next) { - this.config = config; - this.model = config.model; - this.now = new Date(); - this.req = req; - this.res = res; - - runner(fns, this, next); -} - -/** - * Basic request validation and extraction of grant_type and client creds - * - * @param {Function} done - * @this OAuth - */ -function extractCredentials (done) { - // Only POST via application/x-www-form-urlencoded is acceptable - if (this.req.method !== 'POST' || - !this.req.is('application/x-www-form-urlencoded')) { - return done(error('invalid_request', - 'Method must be POST with application/x-www-form-urlencoded encoding')); - } - - // Grant type - this.grantType = this.req.body && this.req.body.grant_type; - if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) { - return done(error('invalid_request', - 'Invalid or missing grant_type parameter')); - } - - // Extract credentials - // http://tools.ietf.org/html/rfc6749#section-3.2.1 - this.client = credsFromBasic(this.req) || credsFromBody(this.req); - if (!this.client.clientId || - !this.client.clientId.match(this.config.regex.clientId)) { - return done(error('invalid_client', - 'Invalid or missing client_id parameter')); - } else if (!this.client.clientSecret) { - return done(error('invalid_client', 'Missing client_secret parameter')); - } - - done(); -} - -/** - * Client Object (internal use only) - * - * @param {String} id client_id - * @param {String} secret client_secret - */ -function Client (id, secret) { - this.clientId = id; - this.clientSecret = secret; -} - -/** - * Extract client creds from Basic auth - * - * @return {Object} Client - */ -function credsFromBasic (req) { - var user = auth(req); - - if (!user) return false; - - return new Client(user.name, user.pass); -} - -/** - * Extract client creds from body - * - * @return {Object} Client - */ -function credsFromBody (req) { - return new Client(req.body.client_id, req.body.client_secret); -} - -/** - * Check extracted client against model - * - * @param {Function} done - * @this OAuth - */ -function checkClient (done) { - var self = this; - this.model.getClient(this.client.clientId, this.client.clientSecret, - function (err, client) { - if (err) return done(error('server_error', false, err)); - - if (!client) { - return done(error('invalid_client', 'Client credentials are invalid')); - } - - // Expose validated client - self.req.oauth = { client: client }; - - done(); - }); -} - -/** - * Delegate to the relvant grant function based on grant_type - * - * @param {Function} done - * @this OAuth - */ -function checkGrantType (done) { - if (this.grantType.match(/^[a-zA-Z][a-zA-Z0-9+.-]+:/) - && this.model.extendedGrant) { - return useExtendedGrant.call(this, done); - } - - switch (this.grantType) { - case 'authorization_code': - return useAuthCodeGrant.call(this, done); - case 'password': - return usePasswordGrant.call(this, done); - case 'refresh_token': - return useRefreshTokenGrant.call(this, done); - case 'client_credentials': - return useClientCredentialsGrant.call(this, done); - default: - done(error('invalid_request', - 'Invalid grant_type parameter or parameter missing')); - } -} - -/** - * Grant for authorization_code grant type - * - * @param {Function} done - */ -function useAuthCodeGrant (done) { - var code = this.req.body.code; - - if (!code) { - return done(error('invalid_request', 'No "code" parameter')); - } - - var self = this; - this.model.getAuthCode(code, function (err, authCode) { - if (err) return done(error('server_error', false, err)); - - if (!authCode || authCode.clientId !== self.client.clientId) { - return done(error('invalid_grant', 'Invalid code')); - } else if (authCode.expires < self.now) { - return done(error('invalid_grant', 'Code has expired')); - } - - self.user = authCode.user || { id: authCode.userId }; - if (!self.user.id) { - return done(error('server_error', false, - 'No user/userId parameter returned from getauthCode')); - } - - done(); - }); -} - -/** - * Grant for password grant type - * - * @param {Function} done - */ -function usePasswordGrant (done) { - // User credentials - var uname = this.req.body.username, - pword = this.req.body.password; - if (!uname || !pword) { - return done(error('invalid_client', - 'Missing parameters. "username" and "password" are required')); - } - - var self = this; - return this.model.getUser(uname, pword, function (err, user) { - if (err) return done(error('server_error', false, err)); - if (!user) { - return done(error('invalid_grant', 'User credentials are invalid')); - } - - self.user = user; - done(); - }); -} - -/** - * Grant for refresh_token grant type - * - * @param {Function} done - */ -function useRefreshTokenGrant (done) { - var token = this.req.body.refresh_token; - - if (!token) { - return done(error('invalid_request', 'No "refresh_token" parameter')); - } - - var self = this; - this.model.getRefreshToken(token, function (err, refreshToken) { - if (err) return done(error('server_error', false, err)); - - if (!refreshToken || refreshToken.clientId !== self.client.clientId) { - return done(error('invalid_grant', 'Invalid refresh token')); - } else if (refreshToken.expires !== null && - refreshToken.expires < self.now) { - return done(error('invalid_grant', 'Refresh token has expired')); - } - - if (!refreshToken.user && !refreshToken.userId) { - return done(error('server_error', false, - 'No user/userId parameter returned from getRefreshToken')); - } - - self.user = refreshToken.user || { id: refreshToken.userId }; - - if (self.model.revokeRefreshToken) { - return self.model.revokeRefreshToken(token, function (err) { - if (err) return done(error('server_error', false, err)); - done(); - }); - } - - done(); - }); -} - -/** - * Grant for client_credentials grant type - * - * @param {Function} done - */ -function useClientCredentialsGrant (done) { - // Client credentials - var clientId = this.client.clientId, - clientSecret = this.client.clientSecret; - - if (!clientId || !clientSecret) { - return done(error('invalid_client', - 'Missing parameters. "client_id" and "client_secret" are required')); - } - - var self = this; - return this.model.getUserFromClient(clientId, clientSecret, - function (err, user) { - if (err) return done(error('server_error', false, err)); - if (!user) { - return done(error('invalid_grant', 'Client credentials are invalid')); - } - - self.user = user; - done(); - }); -} - -/** - * Grant for extended (http://*) grant type - * - * @param {Function} done - */ -function useExtendedGrant (done) { - var self = this; - this.model.extendedGrant(this.grantType, this.req, - function (err, supported, user) { - if (err) { - return done(error(err.error || 'server_error', - err.description || err.message, err)); - } - - if (!supported) { - return done(error('invalid_request', - 'Invalid grant_type parameter or parameter missing')); - } else if (!user || user.id === undefined) { - return done(error('invalid_request', 'Invalid request.')); - } - - self.user = user; - done(); - }); -} - -/** - * Check the grant type is allowed for this client - * - * @param {Function} done - * @this OAuth - */ -function checkGrantTypeAllowed (done) { - this.model.grantTypeAllowed(this.client.clientId, this.grantType, - function (err, allowed) { - if (err) return done(error('server_error', false, err)); - - if (!allowed) { - return done(error('invalid_client', - 'The grant type is unauthorised for this client_id')); - } - - done(); - }); -} - -/** - * Expose user - * - * @param {Function} done - * @this OAuth - */ -function exposeUser (done) { - this.req.user = this.user; - - done(); -} - -/** - * Generate an access token - * - * @param {Function} done - * @this OAuth - */ -function generateAccessToken (done) { - var self = this; - token(this, 'accessToken', function (err, token) { - self.accessToken = token; - done(err); - }); -} - -/** - * Save access token with model - * - * @param {Function} done - * @this OAuth - */ -function saveAccessToken (done) { - var accessToken = this.accessToken; - - // Object idicates a reissue - if (typeof accessToken === 'object' && accessToken.accessToken) { - this.accessToken = accessToken.accessToken; - return done(); - } - - var expires = null; - if (this.config.accessTokenLifetime !== null) { - expires = new Date(this.now); - expires.setSeconds(expires.getSeconds() + this.config.accessTokenLifetime); - } - - this.model.saveAccessToken(accessToken, this.client.clientId, expires, - this.user, function (err) { - if (err) return done(error('server_error', false, err)); - done(); - }); -} - -/** - * Generate a refresh token - * - * @param {Function} done - * @this OAuth - */ -function generateRefreshToken (done) { - if (this.config.grants.indexOf('refresh_token') === -1) return done(); - - var self = this; - token(this, 'refreshToken', function (err, token) { - self.refreshToken = token; - done(err); - }); -} - -/** - * Save refresh token with model - * - * @param {Function} done - * @this OAuth - */ -function saveRefreshToken (done) { - var refreshToken = this.refreshToken; - - if (!refreshToken) return done(); - - // Object idicates a reissue - if (typeof refreshToken === 'object' && refreshToken.refreshToken) { - this.refreshToken = refreshToken.refreshToken; - return done(); - } - - var expires = null; - if (this.config.refreshTokenLifetime !== null) { - expires = new Date(this.now); - expires.setSeconds(expires.getSeconds() + this.config.refreshTokenLifetime); - } - - this.model.saveRefreshToken(refreshToken, this.client.clientId, expires, - this.user, function (err) { - if (err) return done(error('server_error', false, err)); - done(); - }); -} - -/** - * Create an access token and save it with the model - * - * @param {Function} done - * @this OAuth - */ -function sendResponse (done) { - var response = { - token_type: 'bearer', - access_token: this.accessToken - }; - - if (this.config.accessTokenLifetime !== null) { - response.expires_in = this.config.accessTokenLifetime; - } - - if (this.refreshToken) response.refresh_token = this.refreshToken; - - this.res.set({'Cache-Control': 'no-store', 'Pragma': 'no-cache'}); - this.res.jsonp(response); - - if (this.config.continueAfterResponse) - done(); -} diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js new file mode 100644 index 000000000..218dd8986 --- /dev/null +++ b/lib/handlers/authenticate-handler.js @@ -0,0 +1,145 @@ + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var InvalidRequestError = require('../errors/invalid-request-error'); +var InvalidTokenError = require('../errors/invalid-token-error'); +var Promise = require('bluebird'); +var Request = require('../request'); +var ServerError = require('../errors/server-error'); + +/** + * Constructor. + */ + +function AuthenticateHandler(options) { + options = options || {}; + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAccessToken) { + throw new ServerError('Server error: model does not implement `getAccessToken()`'); + } + + this.model = options.model; +} + +/** + * Authenticate Handler. + */ + +AuthenticateHandler.prototype.handle = function(request) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); + } + + return this.getToken(request) + .bind(this) + .then(this.getAccessToken); +}; + +/** + * Get the token from the header or body, depending on the request. + */ + +AuthenticateHandler.prototype.getToken = Promise.method(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 InvalidRequestError('Invalid request: no access token given'); +}); + +/** + * Get the token from the request header. + * + * (See: http://tools.ietf.org/html/rfc6750#section-2.1) + */ + +AuthenticateHandler.prototype.getTokenFromRequestHeader = Promise.method(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. + * + * (See: http://tools.ietf.org/html/rfc6750#section-2.3) + */ + +AuthenticateHandler.prototype.getTokenFromRequestQuery = Promise.method(function() { + throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); +}); + +/** + * Get the token from the request body. + * + * (See: http://tools.ietf.org/html/rfc6750#section-2.2) + */ + +AuthenticateHandler.prototype.getTokenFromRequestBody = Promise.method(function(request) { + if ('GET' === request.method) { + 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 = Promise.method(function(token) { + return Promise.try(this.model.getAccessToken, token) + .then(function(accessToken) { + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (accessToken.expires && accessToken.expires < new Date()) { + throw new InvalidTokenError('Invalid token: access token has expired'); + } + + if (!accessToken.user) { + throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); + } + + return accessToken; + }); +}); + +/** + * Export constructor. + */ + +module.exports = AuthenticateHandler; diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js new file mode 100644 index 000000000..73c7f11ad --- /dev/null +++ b/lib/handlers/authorize-handler.js @@ -0,0 +1,256 @@ + +/** + * 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 OAuthError = require('../errors/oauth-error'); +var Promise = require('bluebird'); +var Request = require('../request'); +var Response = require('../response'); +var ServerError = require('../errors/server-error'); +var tokenUtil = require('../utils/token-util'); +var url = require('url'); + +/** + * Response types. + */ + +var responseTypes = { + code: require('../response-types/code-response-type'), + token: require('../response-types/token-response-type') +}; + +/** + * Constructor. + */ + +function AuthorizeHandler(options) { + options = options || {}; + + if (!options.authCodeLifetime) { + throw new InvalidArgumentError('Missing parameter: `authCodeLifetime`'); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new ServerError('Server error: model does not implement `getClient()`'); + } + + if (!options.model.saveAuthCode) { + throw new ServerError('Server error: model does not implement `saveAuthCode()`'); + } + + this.authCodeLifetime = options.authCodeLifetime; + this.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')); + } + + var fns = [ + this.generateAuthCode(), + this.getAuthCodeLifetime(), + this.getClient(request), + this.getUser(request), + this.getState(request) + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(authCode, expiresOn, client, user, state) { + return this.saveAuthCode(authCode, expiresOn, client, user) + .bind(this) + .then(function(code) { + var responseType = this.getResponseType(request, code); + var redirectUri = this.buildSuccessRedirectUri(client.redirectUri, responseType); + + this.updateResponse(response, redirectUri, state); + + return code; + }) + .catch(function(e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e.message); + } + + var redirectUri = this.buildErrorRedirectUri(client.redirectUri, e); + + this.updateResponse(response, redirectUri, state); + + throw e; + }); + }); +}; + +/** + * Generate auth code. + */ + +AuthorizeHandler.prototype.generateAuthCode = Promise.method(function() { + if (this.model.generateAuthCode) { + return this.model.generateAuthCode(); + } + + return tokenUtil.generateRandomToken(); +}); + +/** + * Get auth code lifetime. + */ + +AuthorizeHandler.prototype.getAuthCodeLifetime = Promise.method(function() { + var expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.authCodeLifetime); + + return expires; +}); + +/** + * Get the client from the model. + */ + +AuthorizeHandler.prototype.getClient = Promise.method(function(request) { + var clientId = request.body.client_id || request.query.client_id; + + if (!clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + return Promise.try(this.model.getClient, clientId) + .then(function(client) { + if (!client) { + throw new InvalidClientError('Invalid client: client credentials are invalid'); + } + + if (!client.redirectUri) { + throw new InvalidClientError('Invalid client: missing client `redirectUri`'); + } + + return client; + }); +}); + +/** + * Get state from the request. + */ + +AuthorizeHandler.prototype.getState = Promise.method(function(request) { + var state = request.body.state || request.query.state; + + if (!state) { + throw new InvalidRequestError('Missing parameter: `state`'); + } + + return state; +}); + +/** + * Get user by calling the authenticate middleware. + */ + +AuthorizeHandler.prototype.getUser = Promise.method(function(request) { + return this.authenticateHandler.handle(request).then(function(token) { + return token.user; + }); +}); + +/** + * Save auth code. + */ + +AuthorizeHandler.prototype.saveAuthCode = Promise.method(function(authCode, expiresOn, client, user) { + var code = { + authCode: authCode, + expiresOn: expiresOn + }; + + return this.model.saveAuthCode(code, client, user); +}); + +/** + * Get response type. + */ + +AuthorizeHandler.prototype.getResponseType = function(request, code) { + var responseType = request.body.response_type || request.query.response_type; + + if (!responseType) { + throw new InvalidRequestError('Missing parameter: `response_type`'); + } + + if (!_.contains(['code'], responseType)) { + throw new InvalidRequestError('Invalid parameter: `response_type`'); + } + + var Type = responseTypes[responseType]; + + return new Type(code.authCode); +}; + +/** + * Build a successful response that redirects the user-agent to the client-provided url. + */ + +AuthorizeHandler.prototype.buildSuccessRedirectUri = function(redirectUri, responseType) { + return responseType.getRedirectUri(redirectUri); +}; + +/** + * Build an error response that redirects the user-agent to the client-provided url. + */ + +AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) { + var uri = url.parse(redirectUri); + + uri.query = { + error: error.name + }; + + if (error.message) { + uri.query.error_description = error.message; + } + + return uri; +}; + +/** + * Update response with the redirect uri and state parameter. + */ + +AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, state) { + redirectUri.query.state = state; + redirectUri = url.format(redirectUri); + + response.redirect(redirectUri); +}; + +/** + * Export constructor. + */ + +module.exports = AuthorizeHandler; diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js new file mode 100644 index 000000000..d91d08d21 --- /dev/null +++ b/lib/handlers/token-handler.js @@ -0,0 +1,299 @@ + +/** + * 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 Request = require('../request'); +var Response = require('../response'); +var ServerError = require('../errors/server-error'); +var UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error'); +var auth = require('basic-auth'); +var tokenUtil = require('../utils/token-util'); + +/** + * 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 ServerError('Server error: model does not implement `getClient()`'); + } + + if (!options.model.saveToken) { + throw new ServerError('Server error: model does not implement `saveToken()`'); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.grantTypes = _.assign({}, grantTypes, options.extendedGrantTypes); + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; +} + +/** + * 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 ('POST' !== request.method) { + 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')); + } + + var fns = [ + this.generateAccessToken(), + this.generateRefreshToken(), + this.getAccessTokenLifetime(), + this.getRefreshTokenLifetime(), + this.getClient(request) + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, client) { + return this.handleGrantType(request, client) + .bind(this) + .then(function(instance) { + return this.getUser(request, instance); + }) + .then(function(user) { + return this.saveToken(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, client, user); + }) + .then(function(token) { + var tokenType = this.getTokenType(token); + + this.updateSuccessResponse(response, tokenType); + + return token; + }) + .catch(function(e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e.message); + } + + this.updateErrorResponse(response, e); + + throw e; + }); + }); +}; + +/** + * Generate access token. + */ + +TokenHandler.prototype.generateAccessToken = Promise.method(function() { + if (this.model.generateAccessToken) { + return this.model.generateAccessToken(); + } + + return tokenUtil.generateRandomToken(); +}); + +/** + * Generate refresh token. + */ + +TokenHandler.prototype.generateRefreshToken = Promise.method(function() { + if (this.model.generateRefreshToken) { + return this.model.generateRefreshToken(); + } + + return tokenUtil.generateRandomToken(); +}); + +/** + * Get access token lifetime. + */ + +TokenHandler.prototype.getAccessTokenLifetime = Promise.method(function() { + var expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); + + return expires; +}); + +/** + * Get refresh token lifetime. + */ + +TokenHandler.prototype.getRefreshTokenLifetime = Promise.method(function() { + var expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); + + return expires; +}); + +/** + * Get the client from the model. + */ + +TokenHandler.prototype.getClient = Promise.method(function(request) { + var credentials = this.getClientCredentials(request); + + return Promise.try(this.model.getClient, [credentials.clientId, credentials.clientSecret]) + .then(function(client) { + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + return client; + }); +}); + +/** + * 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); + + 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'); +}; + +/** + * Handle grant type. + */ + +TokenHandler.prototype.handleGrantType = Promise.method(function(request, client) { + var grantType = request.body.grant_type; + + if (!grantType) { + throw new InvalidRequestError('Missing parameter: `grant_type`'); + } + + if (!_.has(this.grantTypes, grantType)) { + throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid'); + } + + var Type = this.grantTypes[grantType]; + + return new Type(this.model) + .handle(request, client); +}); + +/** + * Get user. + */ + +TokenHandler.prototype.getUser = Promise.method(function(request, instance) { + if ('authorization_code' === request.body.grant_type) { + return instance.user; + } + + if ('refresh_token' === request.body.grant_type) { + return instance.user; + } + + return instance; +}); + +/** + * Save token. + */ + +TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, client, user) { + var token = { + accessToken: accessToken, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshToken: refreshToken, + refreshTokenExpiresOn: refreshTokenExpiresOn + }; + + return this.model.saveToken(token, client, user); +}); + +/** + * Get token type. + */ + +TokenHandler.prototype.getTokenType = function(token) { + return new BearerTokenType(token.accessToken, this.accessTokenLifetime, token.refreshToken); +}; + +/** + * Update response when a token is generated. + */ + +TokenHandler.prototype.updateSuccessResponse = Promise.method(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 = Promise.method(function(response, error) { + response.body = { + error: error.name, + error_description: error.message + }; + + response.status = error.code; +}); + +/** + * Export constructor. + */ + +module.exports = TokenHandler; diff --git a/lib/oauth2server.js b/lib/oauth2server.js deleted file mode 100644 index 2726043b6..000000000 --- a/lib/oauth2server.js +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var error = require('./error'), - AuthCodeGrant = require('./authCodeGrant'), - Authorise = require('./authorise'), - Grant = require('./grant'); - -module.exports = OAuth2Server; - -/** - * Constructor - * - * @param {Object} config Configuration object - */ -function OAuth2Server (config) { - - if (!(this instanceof OAuth2Server)) return new OAuth2Server(config); - - config = config || {}; - - if (!config.model) throw new Error('No model supplied to OAuth2Server'); - this.model = config.model; - - this.grants = config.grants || []; - this.debug = config.debug || function () {}; - if (typeof this.debug !== 'function') { - this.debug = console.log; - } - this.passthroughErrors = config.passthroughErrors; - this.continueAfterResponse = config.continueAfterResponse; - - this.accessTokenLifetime = config.accessTokenLifetime !== undefined ? - config.accessTokenLifetime : 3600; - this.refreshTokenLifetime = config.refreshTokenLifetime !== undefined ? - config.refreshTokenLifetime : 1209600; - this.authCodeLifetime = config.authCodeLifetime || 30; - - this.regex = { - clientId: config.clientIdRegex || /^[a-z0-9-_]{3,40}$/i, - grantType: new RegExp('^(' + this.grants.join('|') + ')$', 'i') - }; -} - -/** - * Authorisation Middleware - * - * Returns middleware that will authorise the request using oauth, - * if successful it will allow the request to proceed to the next handler - * - * @return {Function} middleware - */ -OAuth2Server.prototype.authorise = function () { - var self = this; - - return function (req, res, next) { - return new Authorise(self, req, next); - }; -}; - -/** - * Grant Middleware - * - * Returns middleware that will grant tokens to valid requests. - * This would normally be mounted at '/oauth/token' e.g. - * - * `app.all('/oauth/token', oauth.grant());` - * - * @return {Function} middleware - */ -OAuth2Server.prototype.grant = function () { - var self = this; - - return function (req, res, next) { - new Grant(self, req, res, next); - }; -}; - -/** - * Code Auth Grant Middleware - * - * @param {Function} check Function will be called with req to check if the - * user has authorised the request. - * @return {Function} middleware - */ -OAuth2Server.prototype.authCodeGrant = function (check) { - var self = this; - - return function (req, res, next) { - new AuthCodeGrant(self, req, res, next, check); - }; -}; - -/** - * OAuth Error Middleware - * - * Returns middleware that will catch OAuth errors and ensure an OAuth - * complaint response - * - * @return {Function} middleware - */ -OAuth2Server.prototype.errorHandler = function () { - var self = this; - - return function (err, req, res, next) { - if (!(err instanceof error) || self.passthroughErrors) return next(err); - - delete err.name; - delete err.message; - - self.debug(err.stack || err); - delete err.stack; - - if (err.headers) res.set(err.headers); - delete err.headers; - - res.status(err.code).send(err); - }; -}; - -/** - * Lockdown - * - * When using the lockdown patter, this function should be called after - * all routes have been declared. - * It will search through each route and if it has not been explitly bypassed - * (by passing oauth.bypass) then authorise will be inserted. - * If oauth.grant has been passed it will replace it with the proper grant - * middleware - * NOTE: When using this method, you must PASS the method not CALL the method, - * e.g.: - * - * ` - * app.all('/oauth/token', app.oauth.grant); - * - * app.get('/secrets', function (req, res) { - * res.send('secrets'); - * }); - * - * app.get('/public', app.oauth.bypass, function (req, res) { - * res.send('publci'); - * }); - * - * app.oauth.lockdown(app); - * ` - * - * @param {Object} app Express app - */ -OAuth2Server.prototype.lockdown = function (app) { - var self = this; - - var lockdownExpress3 = function (stack) { - // Check if it's a grant route - var pos = stack.indexOf(self.grant); - if (pos !== -1) { - stack[pos] = self.grant(); - return; - } - - // Check it's not been explitly bypassed - pos = stack.indexOf(self.bypass); - if (pos === -1) { - stack.unshift(self.authorise()); - } else { - stack.splice(pos, 1); - } - }; - - var lockdownExpress4 = function (layer) { - if (!layer.route) - return; - - var stack = layer.route.stack; - var handlers = stack.map(function (item) { - return item.handle; - }); - - // Check if it's a grant route - var pos = handlers.indexOf(self.grant); - if (pos !== -1) { - stack[pos].handle = self.grant(); - return; - } - - // Check it's not been explitly bypassed - pos = handlers.indexOf(self.bypass); - if (pos === -1) { - // Add authorise another route (could do it properly with express.route?) - var copy = {}; - var first = stack[0]; - for (var key in first) { - copy[key] = first[key]; - } - copy.handle = self.authorise(); - stack.unshift(copy); - } else { - stack.splice(pos, 1); - } - }; - - if (app.routes) { - for (var method in app.routes) { - app.routes[method].forEach(function (route) { - lockdownExpress3(route.callbacks); - }); - } - } else { - app._router.stack.forEach(lockdownExpress4); - } -}; - -/** - * Bypass - * - * This is used as placeholder for when using the lockdown pattern - * - * @return {Function} noop - */ -OAuth2Server.prototype.bypass = function () {}; diff --git a/lib/request.js b/lib/request.js new file mode 100644 index 000000000..51623c38f --- /dev/null +++ b/lib/request.js @@ -0,0 +1,58 @@ + +/** + * 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 = options.headers || []; + this.method = options.method; + this.query = options.query; +} + +/** + * 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; +}; + +/** + * Get a request header. + */ + +Request.prototype.get = function(field) { + return this.headers[field.toLowerCase()]; +}; + +/** + * Export constructor. + */ + +module.exports = Request; diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js new file mode 100644 index 000000000..9fae7a9c9 --- /dev/null +++ b/lib/response-types/code-response-type.js @@ -0,0 +1,42 @@ + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var url = require('url'); + +/** + * Constructor. + */ + +function CodeResponseType(code) { + if (!code) { + throw new InvalidArgumentError('Missing parameter: `code`'); + } + + this.code = code; +} + +/** + * Get redirect uri. + */ + +CodeResponseType.prototype.getRedirectUri = function(redirectUri) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + var uri = url.parse(redirectUri, true); + + uri.query.code = this.code; + uri.search = null; + + return uri; +}; + +/** + * Export constructor. + */ + +module.exports = CodeResponseType; diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js new file mode 100644 index 000000000..e45644452 --- /dev/null +++ b/lib/response-types/token-response-type.js @@ -0,0 +1,14 @@ + +/** + * Constructor. + */ + +function TokenResponseType() { + throw new Error('Not implemented.'); +} + +/** + * Export constructor. + */ + +module.exports = TokenResponseType; diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 000000000..67832d31d --- /dev/null +++ b/lib/response.js @@ -0,0 +1,35 @@ + +/** + * Constructor. + */ + +function Response(options) { + options = options || {}; + + this.body = options.body || {}; + this.headers = options.headers || []; + this.status = 200; +} + +/** + * 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/runner.js b/lib/runner.js deleted file mode 100644 index 9b25e6213..000000000 --- a/lib/runner.js +++ /dev/null @@ -1,20 +0,0 @@ - - -module.exports = runner; - -/** - * Run through the sequence of functions - * - * @param {Function} next - * @public - */ -function runner (fns, context, next) { - var last = fns.length - 1; - - (function run(pos) { - fns[pos].call(context, function (err) { - if (err || pos === last) return next(err); - run(++pos); - }); - })(0); -} diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 000000000..3376bc617 --- /dev/null +++ b/lib/server.js @@ -0,0 +1,64 @@ + +/** + * Module dependencies. + */ + +var _ = require('lodash'); +var AuthenticateHandler = require('./handlers/authenticate-handler'); +var AuthorizeHandler = require('./handlers/authorize-handler'); +var InvalidArgumentError = require('./errors/invalid-argument-error'); +var TokenHandler = require('./handlers/token-handler'); + +/** + * Constructor. + */ + +function OAuth2Server(options) { + options = options || {}; + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + this.options = _.assign({ + accessTokenLifetime: 60 * 60, + authCodeLifetime: 5 * 60, + refreshTokenLifetime: 1209600 + }, options); +} + +/** + * Authenticate a token. + */ + +OAuth2Server.prototype.authenticate = function(request, callback) { + return new AuthenticateHandler(this.options) + .handle(request) + .nodeify(callback); +}; + +/** + * Authorize a request. + */ + +OAuth2Server.prototype.authorize = function(request, response, callback) { + return new AuthorizeHandler(this.options) + .handle(request, response) + .nodeify(callback); +}; + +/** + * Create a token. + */ + +OAuth2Server.prototype.token = function(request, response, callback) { + return new TokenHandler(this.options) + .handle(request, response) + .nodeify(callback); +}; + +/** + * Export constructor. + */ + +module.exports = OAuth2Server; diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js new file mode 100644 index 000000000..5b9d16fc3 --- /dev/null +++ b/lib/token-types/bearer-token-type.js @@ -0,0 +1,51 @@ + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); + +/** + * Constructor. + */ + +function BearerTokenType(accessToken, accessTokenLifetime, refreshToken) { + if (!accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } + + if (!accessTokenLifetime) { + throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); + } + + this.accessToken = accessToken; + this.accessTokenLifetime = accessTokenLifetime; + this.refreshToken = refreshToken; +} + +/** + * 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; + } + + return object; +}; + +/** + * Export constructor. + */ + +module.exports = BearerTokenType; diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js new file mode 100644 index 000000000..3a5644e32 --- /dev/null +++ b/lib/token-types/mac-token-type.js @@ -0,0 +1,14 @@ + +/** + * Constructor. + */ + +function MacTokenType() { + throw new Error('Not implemented.'); +} + +/** + * Export constructor. + */ + +module.exports = MacTokenType; diff --git a/lib/token.js b/lib/token.js deleted file mode 100644 index 77423422a..000000000 --- a/lib/token.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var crypto = require('crypto'), - error = require('./error'); - -module.exports = Token; - -/** - * Token generator that will delegate to model or - * the internal random generator - * - * @param {String} type 'accessToken' or 'refreshToken' - * @param {Function} callback - */ -function Token (config, type, callback) { - if (config.model.generateToken) { - config.model.generateToken(type, config.req, function (err, token) { - if (err) return callback(error('server_error', false, err)); - if (!token) return generateRandomToken(callback); - callback(false, token); - }); - } else { - generateRandomToken(callback); - } -} - -/** - * Internal random token generator - * - * @param {Function} callback - */ -var generateRandomToken = function (callback) { - crypto.randomBytes(256, function (ex, buffer) { - if (ex) return callback(error('server_error')); - - var token = crypto - .createHash('sha1') - .update(buffer) - .digest('hex'); - - callback(false, token); - }); -}; diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js new file mode 100644 index 000000000..928f22456 --- /dev/null +++ b/lib/utils/token-util.js @@ -0,0 +1,28 @@ + +/** + * 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/package.json b/package.json index 2fe70df8d..acde84d3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oauth2-server", - "description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with express in node.js", + "description": "Complete, framework-agnostic, compliant and well tested module for implementing an OAuth2 Server in node.js", "version": "2.4.1", "keywords": [ "oauth", @@ -16,28 +16,25 @@ "email": "thom@seddonmedia.co.uk" } ], - "main": "lib/oauth2server.js", + "main": "index.js", "dependencies": { - "basic-auth": "~0.0.1" + "basic-auth": "^1.0.0", + "bluebird": "^2.9.13", + "lodash": "^3.3.1", + "standard-error": "^1.1.0", + "type-is": "^1.6.0" }, "devDependencies": { - "body-parser": "~1.3.1", - "express": "~4.4.3", - "mocha": "~1.20.1", - "should": "~4.0.4", - "supertest": "~0.13.0" + "mocha": "^2.2.1", + "should": "^5.0.1", + "sinon": "^1.13.0" }, - "licenses": [ - { - "type": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - } - ], + "license": "MIT", "engines": { "node": ">=0.8" }, "scripts": { - "test": "mocha" + "test": "NODE_ENV=test ./node_modules/.bin/mocha 'test/**/*_test.js'" }, "repository": { "type": "git", diff --git a/test/assertions.js b/test/assertions.js new file mode 100644 index 000000000..f50024454 --- /dev/null +++ b/test/assertions.js @@ -0,0 +1,16 @@ + +/** + * 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); diff --git a/test/authCodeGrant.js b/test/authCodeGrant.js deleted file mode 100644 index 1ccecfdf3..000000000 --- a/test/authCodeGrant.js +++ /dev/null @@ -1,385 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (model, params, continueAfterResponse) { - - var app = express(); - app.oauth = oauth2server({ - model: model || {}, - continueAfterResponse: continueAfterResponse - }); - - app.use(bodyParser()); - - app.post('/authorise', app.oauth.authCodeGrant(function (req, next) { - next.apply(null, params || []); - })); - - app.get('/authorise', app.oauth.authCodeGrant(function (req, next) { - next.apply(null, params || []); - })); - - app.use(app.oauth.errorHandler()); - - return app; -}; - -describe('AuthCodeGrant', function() { - - it('should detect no response type', function (done) { - var app = bootstrap(); - - request(app) - .post('/authorise') - .expect(400, /invalid response_type parameter/i, done); - }); - - it('should detect invalid response type', function (done) { - var app = bootstrap(); - - request(app) - .post('/authorise') - .send({ response_type: 'token' }) - .expect(400, /invalid response_type parameter/i, done); - }); - - it('should detect no client_id', function (done) { - var app = bootstrap(); - - request(app) - .post('/authorise') - .send({ response_type: 'code' }) - .expect(400, /invalid or missing client_id parameter/i, done); - }); - - it('should detect no redirect_uri', function (done) { - var app = bootstrap(); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom' - }) - .expect(400, /invalid or missing redirect_uri parameter/i, done); - }); - - it('should detect invalid client', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(); // Fake invalid - } - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .expect('WWW-Authenticate', 'Basic realm="Service"') - .expect(400, /invalid client credentials/i, done); - }); - - it('should detect mismatching redirect_uri with a string', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - } - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://wrong.com' - }) - .expect(400, /redirect_uri does not match/i, done); - }); - - it('should detect mismatching redirect_uri within an array', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: ['http://nightworld.com','http://dayworld.com'] - }); - } - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://wrong.com' - }) - .expect(400, /redirect_uri does not match/i, done); - }); - - it('should accept a valid redirect_uri within an array', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: ['http://nightworld.com','http://dayworld.com'] - }); - } - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .expect(302, /Moved temporarily/i, done); - }); - - it('should accept a valid redirect_uri with a string', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - } - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .expect(302, /Moved temporarily/i, done); - }); - - it('should detect user access denied', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - } - }, [false, false]); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .expect(302, - /Redirecting to http:\/\/nightworld.com\?error=access_denied/i, done); - }); - - it('should try to save auth code', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { - should.exist(authCode); - authCode.should.have.lengthOf(40); - clientId.should.equal('thom'); - (+expires).should.be.within(2, (+new Date()) + 30000); - done(); - } - }, [false, true]); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .end(); - }); - - it('should accept valid request and return code using POST', function (done) { - var code; - - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { - should.exist(authCode); - code = authCode; - callback(); - } - }, [false, true]); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .expect(302, function (err, res) { - res.header.location.should.equal('http://nightworld.com?code=' + code); - done(); - }); - }); - - it('should accept valid request and return code using GET', function (done) { - var code; - - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { - should.exist(authCode); - code = authCode; - callback(); - } - }, [false, true]); - - request(app) - .get('/authorise') - .query({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .expect(302, function (err, res) { - res.header.location.should.equal('http://nightworld.com?code=' + code); - done(); - }); - }); - - it('should accept valid request and return code and state using GET', function (done) { - var code; - - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { - should.exist(authCode); - code = authCode; - callback(); - } - }, [false, true]); - - request(app) - .get('/authorise') - .query({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com', - state: 'some_state' - }) - .expect(302, function (err, res) { - res.header.location.should.equal('http://nightworld.com?code=' + code + '&state=some_state'); - done(); - }); - }); - - it('should continue after success response if continueAfterResponse = true', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { - callback(); - } - }, [false, true], true); - - var hit = false; - app.all('*', function (req, res, done) { - hit = true; - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .end(function (err, res) { - if (err) return done(err); - hit.should.equal(true); - done(); - }); - }); - - it('should continue after redirect response if continueAfterResponse = true', function (done) { - var app = bootstrap({ - getClient: function (clientId, clientSecret, callback) { - callback(false, { - clientId: 'thom', - redirectUri: 'http://nightworld.com' - }); - } - }, [false, false], true); - - var hit = false; - app.all('*', function (req, res, done) { - hit = true; - }); - - request(app) - .post('/authorise') - .send({ - response_type: 'code', - client_id: 'thom', - redirect_uri: 'http://nightworld.com' - }) - .end(function (err, res) { - if (err) return done(err); - hit.should.equal(true); - done(); - }); - }); - -}); diff --git a/test/authorise.js b/test/authorise.js deleted file mode 100644 index 0b33f564c..000000000 --- a/test/authorise.js +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - if (oauthConfig === 'mockValid') { - oauthConfig = { - model: { - getAccessToken: function (token, callback) { - token.should.equal('thom'); - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + 20); - callback(false, { expires: expires }); - } - } - }; - } - - var app = express(); - app.oauth = oauth2server(oauthConfig || { model: {} }); - - app.use(bodyParser()); - app.all('/', app.oauth.authorise()); - - - app.all('/', function (req, res) { - res.send('nightworld'); - }); - - app.use(app.oauth.errorHandler()); - - return app; -}; - -describe('Authorise', function() { - - it('should detect no access token', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .get('/') - .expect(400, /the access token was not found/i, done); - }); - - it('should allow valid token as query param', function (done){ - var app = bootstrap('mockValid'); - - request(app) - .get('/?access_token=thom') - .expect(200, /nightworld/, done); - }); - - it('should require application/x-www-form-urlencoded when access token is ' + - 'in body', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .post('/') - .send({ access_token: 'thom' }) - .expect(400, /content type must be application\/x-www-form-urlencoded/i, - done); - }); - - it('should not allow GET when access token in body', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .get('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ access_token: 'thom' }) - .expect(400, /method cannot be GET/i, done); - }); - - it('should allow valid token in body', function (done){ - var app = bootstrap('mockValid'); - - request(app) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ access_token: 'thom' }) - .expect(200, /nightworld/, done); - }); - - it('should detect malformed header', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .get('/') - .set('Authorization', 'Invalid') - .expect(400, /malformed auth header/i, done); - }); - - it('should allow valid token in header', function (done){ - var app = bootstrap('mockValid'); - - request(app) - .get('/') - .set('Authorization', 'Bearer thom') - .expect(200, /nightworld/, done); - }); - - it('should allow exactly one method (get: query + auth)', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .get('/?access_token=thom') - .set('Authorization', 'Invalid') - .expect(400, /only one method may be used/i, done); - }); - - it('should allow exactly one method (post: query + body)', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .post('/?access_token=thom') - .send({ - access_token: 'thom' - }) - .expect(400, /only one method may be used/i, done); - }); - - it('should allow exactly one method (post: query + empty body)', function (done) { - var app = bootstrap('mockValid'); - - request(app) - .post('/?access_token=thom') - .send({ - access_token: '' - }) - .expect(400, /only one method may be used/i, done); - }); - - it('should detect expired token', function (done){ - var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - callback(false, { expires: 0 }); // Fake expires - } - } - }); - - request(app) - .get('/?access_token=thom') - .expect(401, /the access token provided has expired/i, done); - }); - - it('should passthrough with valid, non-expiring token (token = null)', - function (done) { - var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - token.should.equal('thom'); - callback(false, { expires: null }); - } - } - }, false); - - app.get('/', app.oauth.authorise(), function (req, res) { - res.send('nightworld'); - }); - - app.use(app.oauth.errorHandler()); - - request(app) - .get('/?access_token=thom') - .expect(200, /nightworld/, done); - }); - - it('should expose the user id when setting userId', function (done) { - var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + 20); - callback(false, { expires: expires , userId: 1 }); - } - } - }, false); - - app.get('/', app.oauth.authorise(), function (req, res) { - req.should.have.property('user'); - req.user.should.have.property('id'); - req.user.id.should.equal(1); - res.send('nightworld'); - }); - - app.use(app.oauth.errorHandler()); - - request(app) - .get('/?access_token=thom') - .expect(200, /nightworld/, done); - }); - - it('should expose the user id when setting user object', function (done) { - var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + 20); - callback(false, { expires: expires , user: { id: 1, name: 'thom' }}); - } - } - }, false); - - app.get('/', app.oauth.authorise(), function (req, res) { - req.should.have.property('user'); - req.user.should.have.property('id'); - req.user.id.should.equal(1); - req.user.should.have.property('name'); - req.user.name.should.equal('thom'); - res.send('nightworld'); - }); - - app.use(app.oauth.errorHandler()); - - request(app) - .get('/?access_token=thom') - .expect(200, /nightworld/, done); - }); - -}); \ No newline at end of file diff --git a/test/error.js b/test/error.js deleted file mode 100644 index f8f23a853..000000000 --- a/test/error.js +++ /dev/null @@ -1,81 +0,0 @@ -var should = require('should'); - -var OAuth2Error = require('../lib/error'); - -describe('OAuth2Error', function() { - - it('should be an instance of `Error`', function () { - var error = new OAuth2Error('invalid_request', 'The access token was not found'); - - error.should.be.instanceOf(Error); - }); - - it('should expose the `message` as the description', function () { - var error = new OAuth2Error('invalid_request', 'The access token was not found'); - - error.message.should.equal('The access token was not found'); - }); - - it('should expose the `stack`', function () { - var error = new OAuth2Error('invalid_request', 'The access token was not found'); - - error.stack.should.not.equal(undefined); - }); - - it('should expose a custom `name`', function () { - var error = new OAuth2Error(); - - error.name.should.equal('OAuth2Error'); - }); - - it('should set cache `headers`', function () { - var error = new OAuth2Error('invalid_request'); - - error.headers.should.eql({ - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache' - }); - }); - - it('should include WWW-Authenticate `header` if error is `invalid_client`', function () { - var error = new OAuth2Error('invalid_client'); - - error.headers.should.eql({ - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - 'WWW-Authenticate': 'Basic realm="Service"' - }); - }); - - it('should expose a status `code`', function () { - var error = new OAuth2Error('invalid_client'); - - error.code.should.be.instanceOf(Number); - }); - - it('should expose the `error`', function () { - var error = new OAuth2Error('invalid_client'); - - error.error.should.equal('invalid_client'); - }); - - it('should expose the `error_description`', function () { - var error = new OAuth2Error('invalid_client', 'The access token was not found'); - - error.error_description.should.equal('The access token was not found'); - }); - - it('should expose the `stack` of a previous error', function () { - var error = new OAuth2Error('invalid_request', 'The access token was not found', new Error()); - - error.stack.should.not.match(/^OAuth2Error/); - error.stack.should.match(/^Error/); - }); - - it('should expose the `message` of a previous error', function () { - var error = new OAuth2Error('invalid_request', 'The access token was not found', new Error('foo')); - - error.message.should.equal('foo'); - }); - -}); diff --git a/test/errorHandler.js b/test/errorHandler.js deleted file mode 100644 index 8ace75937..000000000 --- a/test/errorHandler.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { model: {} }); - - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - app.all('/', oauth.authorise(), function (req, res) { - res.send('Hello World'); - }); - - app.use(oauth.errorHandler()); - if (oauthConfig && oauthConfig.passthroughErrors) { - app.use(function (err, req, res, next) { - res.send('passthrough'); - }); - } - - return app; -}; - -describe('Error Handler', function() { - it('should return an oauth conformat response', function (done) { - var app = bootstrap(); - - request(app) - .get('/') - .expect(400) - .end(function (err, res) { - if (err) return done(err); - - res.body.should.have.keys('code', 'error', 'error_description'); - - res.body.code.should.be.instanceOf(Number); - res.body.code.should.equal(res.statusCode); - - res.body.error.should.be.instanceOf(String); - - res.body.error_description.should.be.instanceOf(String); - - done(); - }); - }); - - it('should passthrough authorise errors', function (done) { - var app = bootstrap({ - passthroughErrors: true, - model: {} - }); - - request(app) - .get('/') - .expect(200, /^passthrough$/, done); - }); - - it('should passthrough grant errors', function (done) { - var app = bootstrap({ - passthroughErrors: true, - model: {} - }); - - request(app) - .post('/oauth/token') - .expect(200, /^passthrough$/, done); - }); -}); \ No newline at end of file diff --git a/test/grant.authorization_code.js b/test/grant.authorization_code.js deleted file mode 100644 index bfd6f853d..000000000 --- a/test/grant.authorization_code.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { - model: {}, - grants: ['password', 'refresh_token'] - }); - - app.set('json spaces', 0); - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - - app.use(oauth.errorHandler()); - - return app; -}; - -describe('Granting with authorization_code grant type', function () { - it('should detect missing parameters', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - } - }, - grants: ['authorization_code'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'authorization_code', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(400, /no \\"code\\" parameter/i, done); - - }); - - it('should invalid authorization_code', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getAuthCode: function (code, callback) { - callback(false); // Fake invalid - } - }, - grants: ['authorization_code'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'authorization_code', - client_id: 'thom', - client_secret: 'nightworld', - code: 'abc123' - }) - .expect(400, /invalid code/i, done); - }); - - it('should detect invalid client_id', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getAuthCode: function (code, callback) { - callback(false, { client_id: 'wrong' }); - } - }, - grants: ['authorization_code'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'authorization_code', - client_id: 'thom', - client_secret: 'nightworld', - code: 'abc123' - }) - .expect(400, /invalid code/i, done); - }); - - it('should detect expired code', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { client_id: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getAuthCode: function (data, callback) { - callback(false, { - clientId: 'thom', - expires: new Date(+new Date() - 60) - }); - } - }, - grants: ['authorization_code'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'authorization_code', - client_id: 'thom', - client_secret: 'nightworld', - code: 'abc123' - }) - .expect(400, /code has expired/i, done); - }); - - it('should require code expiration', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { client_id: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getAuthCode: function (data, callback) { - callback(false, { - clientId: 'thom', - expires: null // This is invalid - }); - } - }, - grants: ['authorization_code'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'authorization_code', - client_id: 'thom', - client_secret: 'nightworld', - code: 'abc123' - }) - .expect(400, /code has expired/i, done); - }); - - - it('should allow valid request', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { client_id: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getAuthCode: function (refreshToken, callback) { - refreshToken.should.equal('abc123'); - callback(false, { - clientId: 'thom', - expires: new Date(), - userId: '123' - }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - saveRefreshToken: function (data, cb) { - cb(); - }, - expireRefreshToken: function (refreshToken, callback) { - callback(); - } - }, - grants: ['authorization_code'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'authorization_code', - client_id: 'thom', - client_secret: 'nightworld', - code: 'abc123' - }) - .expect(200, /"access_token":"(.*)"/i, done); - }); - -}); diff --git a/test/grant.client_credentials.js b/test/grant.client_credentials.js deleted file mode 100644 index 730946167..000000000 --- a/test/grant.client_credentials.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { - model: {}, - grants: ['client_credentials'] - }); - - app.set('json spaces', 0); - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - - app.use(oauth.errorHandler()); - - return app; -}; - -describe('Granting with client_credentials grant type', function () { - - // N.B. Client is authenticated earlier in request - - it('should detect invalid user', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUserFromClient: function (clientId, clientSecret, callback) { - clientId.should.equal('thom'); - clientSecret.should.equal('nightworld'); - callback(false, false); // Fake invalid user - } - }, - grants: ['client_credentials'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'client_credentials' - }) - .set('Authorization', 'Basic dGhvbTpuaWdodHdvcmxk') - .expect(400, /client credentials are invalid/i, done); - - }); -}); \ No newline at end of file diff --git a/test/grant.extended.js b/test/grant.extended.js deleted file mode 100644 index f75c9bd65..000000000 --- a/test/grant.extended.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { - model: {}, - grants: ['password', 'refresh_token'] - }); - - app.set('json spaces', 0); - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - - app.use(oauth.errorHandler()); - - return app; -}; - -describe('Granting with extended grant type', function () { - it('should ignore if no extendedGrant method', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - } - }, - grants: ['http://custom.com'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'http://custom.com', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(400, /invalid grant_type/i, done); - }); - - it('should still detect unsupported grant_type', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - extendedGrant: function (grantType, req, callback) { - callback(false, false); - } - }, - grants: ['http://custom.com'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'http://custom.com', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(400, /invalid grant_type/i, done); - }); - - it('should require a user.id', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - extendedGrant: function (grantType, req, callback) { - callback(false, true, {}); // Fake empty user - } - }, - grants: ['http://custom.com'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'http://custom.com', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(400, /invalid request/i, done); - }); - - it('should passthrough valid request', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom', clientSecret: 'nightworld' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - extendedGrant: function (grantType, req, callback) { - req.oauth.client.clientId.should.equal('thom'); - req.oauth.client.clientSecret.should.equal('nightworld'); - callback(false, true, { id: 3 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - }, - grants: ['http://custom.com'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'http://custom.com', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(200, done); - }); - - it('should allow any valid URI valid request', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - extendedGrant: function (grantType, req, callback) { - callback(false, true, { id: 3 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - }, - grants: ['urn:custom:grant'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'urn:custom:grant', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(200, done); - }); -}); diff --git a/test/grant.js b/test/grant.js deleted file mode 100644 index 2f345c4e6..000000000 --- a/test/grant.js +++ /dev/null @@ -1,561 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { - model: {}, - grants: ['password', 'refresh_token'] - }); - - app.set('json spaces', 0); - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - - app.use(oauth.errorHandler()); - - return app; -}; - -var validBody = { - grant_type: 'password', - client_id: 'thom', - client_secret: 'nightworld', - username: 'thomseddon', - password: 'nightworld' -}; - -describe('Grant', function() { - - describe('when parsing request', function () { - it('should only allow post', function (done) { - var app = bootstrap(); - - request(app) - .get('/oauth/token') - .expect(400, /method must be POST/i, done); - }); - - it('should only allow application/x-www-form-urlencoded', function (done) { - var app = bootstrap(); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/json') - .send({}) // Required to be valid JSON - .expect(400, /application\/x-www-form-urlencoded/i, done); - }); - - it('should check grant_type exists', function (done) { - var app = bootstrap(); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .expect(400, /invalid or missing grant_type parameter/i, done); - }); - - it('should ensure grant_type is allowed', function (done) { - var app = bootstrap({ model: {}, grants: ['refresh_token'] }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password' }) - .expect(400, /invalid or missing grant_type parameter/i, done); - }); - - it('should check client_id exists', function (done) { - var app = bootstrap(); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password' }) - .expect(400, /invalid or missing client_id parameter/i, done); - }); - - it('should check client_id matches regex', function (done) { - var app = bootstrap({ - clientIdRegex: /match/, - model: {}, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password', client_id: 'thom' }) - .expect(400, /invalid or missing client_id parameter/i, done); - }); - - it('should check client_secret exists', function (done) { - var app = bootstrap(); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password', client_id: 'thom' }) - .expect(400, /missing client_secret parameter/i, done); - }); - - it('should extract credentials from body', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - id.should.equal('thom'); - secret.should.equal('nightworld'); - callback(false, false); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) - .expect(400, done); - }); - - it('should extract credentials from header (Basic)', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - id.should.equal('thom'); - secret.should.equal('nightworld'); - callback(false, false); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .send('grant_type=password&username=test&password=invalid') - .set('Authorization', 'Basic dGhvbTpuaWdodHdvcmxk') - .expect(400, done); - }); - - it('should detect unsupported grant_type', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - } - }, - grants: ['refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) - .expect(400, /invalid or missing grant_type/i, done); - }); - }); - - describe('check client credentials against model', function () { - it('should detect invalid client', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, false); // Fake invalid - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) - .expect(400, /client credentials are invalid/i, done); - }); - }); - - describe('check grant type allowed for client (via model)', function () { - it('should detect grant type not allowed', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, false); // Not allowed - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) - .expect(400, /grant type is unauthorised for this client_id/i, done); - }); - }); - - describe('generate access token', function () { - it('should allow override via model', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - generateToken: function (type, req, callback) { - callback(false, 'thommy'); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - token.should.equal('thommy'); - cb(); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200, /thommy/, done); - - }); - - it('should include client and user in request', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom', clientSecret: 'nightworld' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - generateToken: function (type, req, callback) { - req.oauth.client.clientId.should.equal('thom'); - req.oauth.client.clientSecret.should.equal('nightworld'); - req.user.id.should.equal(1); - callback(false, 'thommy'); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - token.should.equal('thommy'); - cb(); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200, /thommy/, done); - - }); - - it('should reissue if model returns object', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - generateToken: function (type, req, callback) { - callback(false, { accessToken: 'thommy' }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(new Error('Should not be saving')); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200, /"access_token":"thommy"/, done); - - }); - }); - - describe('saving access token', function () { - it('should pass valid params to model.saveAccessToken', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { client_id: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - token.should.be.instanceOf(String); - token.should.have.length(40); - clientId.should.equal('thom'); - user.id.should.equal(1); - (+expires).should.be.within(10, (+new Date()) + 3600000); - cb(); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200, done); - - }); - - it('should pass valid params to model.saveRefreshToken', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { client_id: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - saveRefreshToken: function (token, clientId, expires, user, cb) { - token.should.be.instanceOf(String); - token.should.have.length(40); - clientId.should.equal('thom'); - user.id.should.equal(1); - (+expires).should.be.within(10, (+new Date()) + 1209600000); - cb(); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200, done); - - }); - }); - - describe('issue access token', function () { - it('should return an oauth compatible response', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200) - .expect('Cache-Control', 'no-store') - .expect('Pragma', 'no-cache') - .end(function (err, res) { - if (err) return done(err); - - res.body.should.have.keys(['access_token', 'token_type', 'expires_in']); - res.body.access_token.should.be.instanceOf(String); - res.body.access_token.should.have.length(40); - res.body.token_type.should.equal('bearer'); - res.body.expires_in.should.equal(3600); - - done(); - }); - - }); - - it('should return an oauth compatible response with refresh_token', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { client_id: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - saveRefreshToken: function (token, clientId, expires, user, cb) { - cb(); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200) - .expect('Cache-Control', 'no-store') - .expect('Pragma', 'no-cache') - .end(function (err, res) { - if (err) return done(err); - - res.body.should.have.keys(['access_token', 'token_type', 'expires_in', - 'refresh_token']); - res.body.access_token.should.be.instanceOf(String); - res.body.access_token.should.have.length(40); - res.body.refresh_token.should.be.instanceOf(String); - res.body.refresh_token.should.have.length(40); - res.body.token_type.should.equal('bearer'); - res.body.expires_in.should.equal(3600); - - done(); - }); - - }); - - it('should exclude expires_in if accessTokenLifetime = null', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - should.strictEqual(null, expires); - cb(); - }, - saveRefreshToken: function (token, clientId, expires, user, cb) { - should.strictEqual(null, expires); - cb(); - } - }, - grants: ['password', 'refresh_token'], - accessTokenLifetime: null, - refreshTokenLifetime: null - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - res.body.should.have.keys(['access_token', 'refresh_token', 'token_type']); - res.body.access_token.should.be.instanceOf(String); - res.body.access_token.should.have.length(40); - res.body.refresh_token.should.be.instanceOf(String); - res.body.refresh_token.should.have.length(40); - res.body.token_type.should.equal('bearer'); - - done(); - }); - - }); - - it('should continue after success response if continueAfterResponse1 = true', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - callback(false, { id: 1 }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - } - }, - grants: ['password'], - continueAfterResponse: true - }); - - var hit = false; - app.all('*', function (req, res, done) { - hit = true; - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(validBody) - .expect(200) - .end(function (err, res) { - if (err) return done(err); - hit.should.equal(true); - done(); - }); - }); - - }); - -}); diff --git a/test/grant.password.js b/test/grant.password.js deleted file mode 100644 index 52e722443..000000000 --- a/test/grant.password.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { - model: {}, - grants: ['password', 'refresh_token'] - }); - - app.set('json spaces', 0); - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - - app.use(oauth.errorHandler()); - - return app; -}; - -describe('Granting with password grant type', function () { - it('should detect missing parameters', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'password', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(400, /missing parameters. \\"username\\" and \\"password\\"/i, done); - - }); - - it('should detect invalid user', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getUser: function (uname, pword, callback) { - uname.should.equal('thomseddon'); - pword.should.equal('nightworld'); - callback(false, false); // Fake invalid user - } - }, - grants: ['password'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'password', - client_id: 'thom', - client_secret: 'nightworld', - username: 'thomseddon', - password: 'nightworld' - }) - .expect(400, /user credentials are invalid/i, done); - - }); -}); diff --git a/test/grant.refresh_token.js b/test/grant.refresh_token.js deleted file mode 100644 index 804cff8ef..000000000 --- a/test/grant.refresh_token.js +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); - -var bootstrap = function (oauthConfig) { - var app = express(), - oauth = oauth2server(oauthConfig || { - model: {}, - grants: ['password', 'refresh_token'] - }); - - app.set('json spaces', 0); - app.use(bodyParser()); - - app.all('/oauth/token', oauth.grant()); - - app.use(oauth.errorHandler()); - - return app; -}; - -describe('Granting with refresh_token grant type', function () { - it('should detect missing refresh_token parameter', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld' - }) - .expect(400, /no \\"refresh_token\\" parameter/i, done); - - }); - - it('should detect invalid refresh_token', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getRefreshToken: function (data, callback) { - callback(false, false); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld', - refresh_token: 'abc123' - }) - .expect(400, /invalid refresh token/i, done); - - }); - - it('should detect wrong client id', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, true); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getRefreshToken: function (data, callback) { - callback(false, { client_id: 'kate' }); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld', - refresh_token: 'abc123' - }) - .expect(400, /invalid refresh token/i, done); - - }); - - it('should detect expired refresh token', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getRefreshToken: function (data, callback) { - callback(false, { - clientId: 'thom', - expires: new Date(+new Date() - 60) - }); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld', - refresh_token: 'abc123' - }) - .expect(400, /refresh token has expired/i, done); - - }); - - it('should allow valid request', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getRefreshToken: function (refreshToken, callback) { - refreshToken.should.equal('abc123'); - callback(false, { - clientId: 'thom', - expires: new Date(), - userId: '123' - }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - saveRefreshToken: function (token, clientId, expires, user, cb) { - cb(); - }, - expireRefreshToken: function (refreshToken, callback) { - callback(); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld', - refresh_token: 'abc123' - }) - .expect(200, /"access_token":"(.*)",(.*)"refresh_token":"(.*)"/i, done); - - }); - - it('should allow valid request with user object', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getRefreshToken: function (refreshToken, callback) { - refreshToken.should.equal('abc123'); - callback(false, { - clientId: 'thom', - expires: new Date(), - user: { - id: '123' - } - }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - saveRefreshToken: function (token, clientId, expires, user, cb) { - cb(); - }, - expireRefreshToken: function (refreshToken, callback) { - callback(); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld', - refresh_token: 'abc123' - }) - .expect(200, /"access_token":"(.*)",(.*)"refresh_token":"(.*)"/i, done); - - }); - - it('should allow valid request with non-expiring token (token= null)', function (done) { - var app = bootstrap({ - model: { - getClient: function (id, secret, callback) { - callback(false, { clientId: 'thom' }); - }, - grantTypeAllowed: function (clientId, grantType, callback) { - callback(false, true); - }, - getRefreshToken: function (data, callback) { - callback(false, { - clientId: 'thom', - expires: null, - userId: '123' - }); - }, - saveAccessToken: function (token, clientId, expires, user, cb) { - cb(); - }, - saveRefreshToken: function (token, clientId, expires, user, cb) { - cb(); - }, - expireRefreshToken: function (refreshToken, callback) { - callback(); - } - }, - grants: ['password', 'refresh_token'] - }); - - request(app) - .post('/oauth/token') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send({ - grant_type: 'refresh_token', - client_id: 'thom', - client_secret: 'nightworld', - refresh_token: 'abc123' - }) - .expect(200, /"access_token":"(.*)",(.*)"refresh_token":"(.*)"/i, done); - - }); -}); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js new file mode 100644 index 000000000..b5fdc5576 --- /dev/null +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -0,0 +1,231 @@ + +/** + * 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 sinon = require('sinon'); +var should = require('should'); + +/** + * Test `AuthorizationCodeGrantType`. + */ + +describe('AuthorizationCodeGrantType', 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 `getAuthCode()`', function() { + try { + new AuthorizationCodeGrantType({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `getAuthCode()`'); + } + }); + + it('should set the `model`', function() { + var model = { + getAuthCode: function() {} + }; + var grantType = new AuthorizationCodeGrantType(model); + + grantType.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var grantType = new AuthorizationCodeGrantType({ getAuthCode: function() {} }); + + 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 client = {}; + var model = { + getAuthCode: function() { + return Promise.resolve({}); + } + }; + var grantType = new AuthorizationCodeGrantType(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: `getAuthCode()` did not return a `client` object'); + }); + }); + + it('should throw an error if the request body does not contain `code`', function() { + var client = {}; + var grantType = new AuthorizationCodeGrantType({ getAuthCode: function() {} }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `code`'); + }); + }); + + it('should throw an error if `authCode` is missing', function() { + var client = {}; + var model = { + getAuthCode: function() { + return Promise.resolve(); + } + }; + var grantType = new AuthorizationCodeGrantType(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(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code is invalid'); + }); + }); + + it('should throw an error if `authCode.client` is missing', function() { + var client = {}; + var model = { + getAuthCode: function() { + return Promise.resolve({}); + } + }; + var grantType = new AuthorizationCodeGrantType(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: `getAuthCode()` did not return a `client` object'); + }); + }); + + it('should throw an error if `authCode.user` is missing', function() { + var client = {}; + var model = { + getAuthCode: function() { + return Promise.resolve({ client: {} }); + } + }; + var grantType = new AuthorizationCodeGrantType(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: `getAuthCode()` 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 = { + getAuthCode: function() { + return { client: { id: 456 }, user: {} }; + } + }; + var grantType = new AuthorizationCodeGrantType(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(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 model = { + getAuthCode: function() { + return Promise.resolve({ client: { id: 123 }, expires: new Date() / 10, user: {} }); + } + }; + var grantType = new AuthorizationCodeGrantType(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(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code has expired'); + }); + }); + + it('should return an auth code', function() { + var authCode = { authCode: 12345, client: {}, user: {} }; + var client = {}; + var model = { + getAuthCode: sinon.stub().returns(authCode) + }; + var grantType = new AuthorizationCodeGrantType(model); + var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, client).then(function(data) { + data.should.equal(authCode); + }); + }); + + it('should support promises', function() { + var authCode = { authCode: 12345, client: {}, user: {} }; + var client = {}; + var model = { + getAuthCode: function() { + return Promise.resolve(authCode); + } + }; + var grantType = new AuthorizationCodeGrantType(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 authCode = { authCode: 12345, client: {}, user: {} }; + var client = {}; + var model = { + getAuthCode: function() { + return authCode; + } + }; + var grantType = new AuthorizationCodeGrantType(model); + var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); + + grantType.handle(request, client).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 new file mode 100644 index 000000000..518f7a257 --- /dev/null +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -0,0 +1,137 @@ + +/** + * 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 ServerError = require('../../../lib/errors/server-error'); +var sinon = require('sinon'); +var should = require('should'); + +/** + * Test `ClientCredentialsGrantType`. + */ + +describe('ClientCredentialsGrantType', 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({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `getUserFromClient()`'); + } + }); + + it('should set the `model`', function() { + var model = { + getUserFromClient: function() {} + }; + var grantType = new ClientCredentialsGrantType(model); + + grantType.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); + + 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 grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); + 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 throw an error if `user` is missing', function() { + var model = { + getUserFromClient: function() {} + }; + var grantType = new ClientCredentialsGrantType(model); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return grantType.handle(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: sinon.stub().returns(user) + }; + var grantType = new ClientCredentialsGrantType(model); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, {}) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + + it('should support promises when calling `model.getUserFromClient()`', function() { + var user = { email: 'foo@bar.com' }; + var model = { + getUserFromClient: function() { + return Promise.resolve(user); + } + }; + var grantType = new ClientCredentialsGrantType(model); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + grantType.handle(request, {}).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises when calling `model.getUserFromClient()`', function() { + var user = { email: 'foo@bar.com' }; + var model = { + getUserFromClient: function() { + return user; + } + }; + var grantType = new ClientCredentialsGrantType(model); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + grantType.handle(request, {}).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 new file mode 100644 index 000000000..e81b60a26 --- /dev/null +++ b/test/integration/grant-types/password-grant-type_test.js @@ -0,0 +1,146 @@ + +/** + * 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 ServerError = require('../../../lib/errors/server-error'); +var sinon = require('sinon'); +var should = require('should'); + +/** + * Test `PasswordGrantType`. + */ + +describe('PasswordGrantType', 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({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `getUser()`'); + } + }); + + it('should set the `model`', function() { + var model = { getUser: function() {} }; + var grantType = new PasswordGrantType(model); + + grantType.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var grantType = new PasswordGrantType({ getUser: function() {} }); + + 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 the request body does not contain `username`', function() { + var grantType = new PasswordGrantType({ getUser: function() {} }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return grantType.handle(request) + .then(should.fail) + .catch(function(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 grantType = new PasswordGrantType({ getUser: function() {} }); + var request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `password`'); + }); + }); + + it('should throw an error if `user` is missing', function() { + var model = { + getUser: function() { + return Promise.resolve(); + } + }; + var grantType = new PasswordGrantType(model); + var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(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: sinon.stub().returns(user) + }; + var grantType = new PasswordGrantType(model); + var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request).then(function(data) { + data.should.equal(user); + }); + }); + + it('should support promises when calling `model.getUser()`', function() { + var user = { email: 'foo@bar.com' }; + var model = { + getUser: function() { + return Promise.resolve(user); + } + }; + var grantType = new PasswordGrantType(model); + var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); + + grantType.handle(request).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises when calling `model.getUser()`', function() { + var user = { email: 'foo@bar.com' }; + var model = { + getUser: function() { + return user; + } + }; + var grantType = new PasswordGrantType(model); + var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); + + grantType.handle(request).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 new file mode 100644 index 000000000..7778a6ddc --- /dev/null +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -0,0 +1,206 @@ + +/** + * 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 RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); +var Promise = require('bluebird'); +var Request = require('../../../lib/request'); +var ServerError = require('../../../lib/errors/server-error'); +var sinon = require('sinon'); +var should = require('should'); + +/** + * Test `RefreshTokenGrantType`. + */ + +describe('RefreshTokenGrantType', 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({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `getRefreshToken()`'); + } + }); + + it('should set the `model`', function() { + var model = { getRefreshToken: function() {} }; + var grantType = new RefreshTokenGrantType(model); + + grantType.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + + 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 `refreshToken.client` is missing', function() { + var client = {}; + var model = { + getRefreshToken: function() { + return Promise.resolve({ expires: new Date() * 10 }); + } + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 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: `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 Promise.resolve({ client: {}, expires: new Date() * 10 }); + } + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 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: `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 { client: { id: 456 }, user: {} }; + } + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + + return grantType.handle(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 the request body does not contain `refresh_token`', function() { + var client = {}; + var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `refresh_token`'); + }); + }); + + it('should throw an error if `refresh_token` is missing', function() { + var client = {}; + var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + + return grantType.handle(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 = {}; + var model = { + getRefreshToken: function() { + return Promise.resolve({ client: {}, expires: new Date() / 10, user: {} }); + } + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + + return grantType.handle(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 return a refresh token', function() { + var client = {}; + var refreshToken = { client: {}, expires: new Date() * 10, user: {} }; + var model = { + getRefreshToken: sinon.stub().returns(refreshToken) + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, client).then(function(data) { + data.should.equal(refreshToken); + }); + }); + + it('should support promises when calling `model.getRefreshToken()`', function() { + var client = {}; + var refreshToken = { client: {}, user: {} }; + var model = { + getRefreshToken: function() { + return Promise.resolve(refreshToken); + } + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises when calling `model.getRefreshToken()`', function() { + var client = {}; + var refreshToken = { client: {}, user: {} }; + var model = { + getRefreshToken: function() { + return refreshToken; + } + }; + var grantType = new RefreshTokenGrantType(model); + var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + }); +}); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js new file mode 100644 index 000000000..a075c742f --- /dev/null +++ b/test/integration/handlers/authenticate-handler_test.js @@ -0,0 +1,308 @@ + +/** + * Module dependencies. + */ + +var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); +var InvalidArgumentError = require('../../../lib/errors/invalid-argument-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 ServerError = require('../../../lib/errors/server-error'); +var should = require('should'); + +/** + * Test `AuthenticateHandler`. + */ + +describe('AuthenticateHandler', 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(ServerError); + e.message.should.equal('Server error: model does not implement `getAccessToken()`'); + } + }); + + it('should throw an error if `scope` was given and the model does not implement `validateScope()`', function() { + try { + new AuthenticateHandler({ model: { getAccessToken: function() {} }, scope: 'foobar' }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `validateScope()`'); + } + }); + + it('should set the `model`', function() { + var model = { getAccessToken: function() {} }; + var grantType = new AuthenticateHandler({ model: model }); + + grantType.model.should.equal(model); + }); + }); + + 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 return an access token', function() { + var accessToken = { user: {} }; + var model = { + getAccessToken: function() { + return accessToken; + } + }; + var handler = new AuthenticateHandler({ model: model }); + var request = new Request({ + body: {}, + headers: { 'Authorization': 'Bearer foo' }, + method: {}, + query: {} + }); + + return handler.handle(request).then(function(data) { + data.should.equal(accessToken); + }); + }); + }); + + describe('getToken()', 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' } + }); + + return handler.getToken(request) + .then(should.fail) + .catch(function(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: {} }); + + return handler.getToken(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: no access token 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: {} + }); + + return handler.getTokenFromRequestHeader(request) + .then(should.fail) + .catch(function(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: {} + }); + + handler.getTokenFromRequestHeader(request).then(function(bearerToken) { + 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() {} } }); + + return handler.getTokenFromRequestQuery() + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: do not send bearer tokens in query URLs'); + }); + }); + }); + + 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: {} + }); + + return handler.getTokenFromRequestBody(request) + .then(should.fail) + .catch(function(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: {} + }); + + return handler.getTokenFromRequestBody(request) + .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 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` is expired', function() { + var accessToken = { expires: new Date() / 10 }; + var model = { + getAccessToken: function() { + return accessToken; + } + }; + var handler = new AuthenticateHandler({ model: model }); + + return handler.getAccessToken('foo').catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token has expired'); + }); + }); + + 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); + }); + }); + + 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); + }); + }); +}); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js new file mode 100644 index 000000000..9c9e2540b --- /dev/null +++ b/test/integration/handlers/authorize-handler_test.js @@ -0,0 +1,713 @@ + +/** + * 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 Promise = require('bluebird'); +var Request = require('../../../lib/request'); +var Response = require('../../../lib/response'); +var ServerError = require('../../../lib/errors/server-error'); +var should = require('should'); +var url = require('url'); + +/** + * Test `AuthorizeHandler`. + */ + +describe('AuthorizeHandler', function() { + describe('constructor()', function() { + it('should throw an error if `options.authCodeLifetime` is missing', function() { + try { + new AuthorizeHandler(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `authCodeLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', function() { + try { + new AuthorizeHandler({ authCodeLifetime: 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({ authCodeLifetime: 120, model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `getClient()`'); + } + }); + + it('should throw an error if the model does not implement `saveAuthCode()`', function() { + try { + new AuthorizeHandler({ authCodeLifetime: 120, model: { getClient: function() {} } }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `saveAuthCode()`'); + } + }); + + it('should throw an error if the model does not implement `getAccessToken()`', function() { + var model = { + getClient: function() {}, + saveAuthCode: function() {} + }; + + try { + new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `getAccessToken()`'); + } + }); + + it('should set the `authCodeLifetime`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.authCodeLifetime.should.equal(120); + }); + + it('should set the `authenticateHandler`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); + }); + + it('should set the `model`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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 an error is thrown', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + }, + getClient: function() { + return { redirectUri: 'http://example.com/cb' }; + }, + saveAuthCode: function() { + throw new AccessDeniedError('Cannot request this auth code'); + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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(e) { + 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 = { redirectUri: 'http://example.com/cb' }; + var model = { + getAccessToken: function() { + return { client: client, user: {} }; + }, + getClient: function() { + return client; + }, + saveAuthCode: function() { + return { authCode: 12345, client: client }; + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ + body: { + client_id: 12345, + response_type: 'code' + }, + headers: { + 'Authorization': 'Bearer foo' + }, + method: {}, + query: { + state: 'foobar' + } + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(function() { + response.get('location').should.equal('http://example.com/cb?code=12345&state=foobar'); + }) + .catch(should.fail); + }); + + it('should return the `code` if successful', function() { + var client = { redirectUri: 'http://example.com/cb' }; + var model = { + getAccessToken: function() { + return { client: client, user: {} }; + }, + getClient: function() { + return client; + }, + saveAuthCode: function() { + return { authCode: 12345, client: client }; + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ + body: { + client_id: 12345, + response_type: 'code' + }, + headers: { + 'Authorization': 'Bearer foo' + }, + method: {}, + query: { + state: 'foobar' + } + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(function(data) { + data.should.eql({ + authCode: 12345, + client: { redirectUri: 'http://example.com/cb' } + }); + }) + .catch(should.fail); + }); + }); + + describe('generateAuthCode()', function() { + it('should return an auth code', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + return handler.generateAuthCode().then(function(data) { + data.should.be.a.sha1; + }); + }); + + it('should support promises', function() { + var model = { + generateAuthCode: function() { + return Promise.resolve({}); + }, + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.generateAuthCode().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var model = { + generateAuthCode: function() { + return {}; + }, + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.generateAuthCode().should.be.an.instanceOf(Promise); + }); + }); + + describe('getAuthCodeLifetime()', function() { + it('should return a date', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + return handler.getAuthCodeLifetime().then(function(data) { + data.should.be.an.instanceOf(Date); + }); + }); + }); + + describe('getClient()', function() { + it('should throw an error if `client_id` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `client_id`'); + }); + }); + + it('should throw an error if `client` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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.redirectUri` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() { return {} }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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 support promises', function() { + var model = { + getAccessToken: function() {}, + getClient: function() { + return Promise.resolve({ redirectUri: 'http://example.com/cb' }); + }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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 { redirectUri: 'http://example.com/cb' }; + }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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 body', function() { + it('should return a client', function() { + var client = { redirectUri: 'http://example.com/cb' }; + var model = { + getAccessToken: function() {}, + getClient: function() { + return client; + }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request).then(function(data) { + data.should.equal(client); + }); + }); + }); + + describe('with `client_id` in the request query', function() { + it('should return a client', function() { + var client = { redirectUri: 'http://example.com/cb' }; + var model = { + getAccessToken: function() {}, + getClient: function() { + return client; + }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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); + }); + }); + }); + }); + + describe('getState()', function() { + it('should throw an error if `state` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return handler.getState(request).catch(function(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() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'ø倣‰' } }); + + return handler.getState(request).catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `state`'); + }); + }); + + describe('with `response_type` in the request body', function() { + it('should return the state', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); + + return handler.getState(request).then(function(data) { + data.should.equal('foobar'); + }); + }); + }); + + describe('with `response_type` in the request query', function() { + it('should return the state', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); + + return handler.getState(request).then(function(data) { + data.should.equal('foobar'); + }); + }); + }); + }); + + describe('getUser()', function() { + it('should return a user', function() { + var user = {}; + var model = { + getAccessToken: function() { + return { user: user }; + }, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); + + return handler.getUser(request).then(function(data) { + data.should.equal(user); + }); + }); + }); + + describe('saveAuthCode()', function() { + it('should return an auth code', function() { + var authCode = {}; + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() { + return authCode; + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + return handler.saveAuthCode('foo', 'bar', 'biz', 'baz').then(function(data) { + data.should.equal(authCode); + }); + }); + + it('should support promises when calling `model.saveAuthCode()`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() { + return Promise.resolve({}); + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.saveAuthCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises when calling `model.saveAuthCode()`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() { + return {}; + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + handler.saveAuthCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); + }); + }); + + describe('buildSuccessRedirectUri()', function() { + it('should return a redirect uri', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var responseType = new CodeResponseType(12345); + var redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); + + url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); + }); + }); + + describe('buildErrorRedirectUri()', function() { + it('should set `error_description` if available', function() { + var error = new InvalidClientError('foo bar'); + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error) + + url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); + }); + + it('should return a redirect uri', function() { + var error = new InvalidClientError(); + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); + + url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client'); + }); + }); + + describe('getResponseType()', function() { + it('should throw an error if `response_type` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + try { + handler.getResponseType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `response_type`'); + } + }); + + it('should throw an error if `response_type` is not `code`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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(InvalidRequestError); + e.message.should.equal('Invalid parameter: `response_type`'); + } + }); + + describe('with `response_type` in the request body', function() { + it('should return a response type', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); + var responseType = handler.getResponseType(request, { authCode: 123 }); + + responseType.should.be.an.instanceOf(CodeResponseType); + }); + }); + + describe('with `response_type` in the request query', function() { + it('should return a response type', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); + var responseType = handler.getResponseType(request, { authCode: 123 }); + + responseType.should.be.an.instanceOf(CodeResponseType); + }); + }); + }); +}); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js new file mode 100644 index 000000000..dcea227cb --- /dev/null +++ b/test/integration/handlers/token-handler_test.js @@ -0,0 +1,878 @@ + +/** + * 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 InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +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 UnsupportedGrantTypeError = require('../../../lib/errors/unsupported-grant-type-error'); +var should = require('should'); +var util = require('util'); + +/** + * Test `TokenHandler`. + */ + +describe('TokenHandler', 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(ServerError); + e.message.should.equal('Server error: model does not implement `getClient()`'); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', function() { + var model = { + getClient: function() {} + }; + + try { + new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: model does not implement `saveToken()`'); + } + }); + + 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 `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() { + return {}; + }, + getUser: function() { + return {}; + }, + saveToken: function() { + throw new Error('Unhandled exception'); + } + }; + 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'); + }); + }); + + it('should update the response if an error is thrown', function() { + var model = { + getClient: function() { + return {}; + }, + getUser: function() { + return {}; + }, + saveToken: function() { + throw new Error('Unhandled exception'); + } + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo' + }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + method: 'POST', + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function() { + response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); + response.status.should.equal(503); + }); + }); + + it('should return a bearer token if successful', function() { + var token = { accessToken: 'foo', refreshToken: 'bar', accessTokenLifetime: 120 }; + var model = { + getClient: function() { + return {}; + }, + getUser: function() { + return {}; + }, + saveToken: function() { + return token; + } + }; + 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' + }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + method: 'POST', + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response).then(function(data) { + data.should.eql(token); + }); + }); + }); + + describe('generateAccessToken()', function() { + it('should return an access token', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.generateAccessToken().then(function(data) { + data.should.be.a.sha1; + }); + }); + + it('should support promises', function() { + var model = { + generateAccessToken: function() { + return Promise.resolve({}); + }, + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + handler.generateAccessToken().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var model = { + generateAccessToken: function() { + return {}; + }, + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + handler.generateAccessToken().should.be.an.instanceOf(Promise); + }); + }); + + describe('generateRefreshToken()', function() { + it('should return a refresh token', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.generateRefreshToken().then(function(data) { + data.should.be.a.sha1; + }); + }); + }); + + describe('getAccessTokenLifetime()', function() { + it('should return a date', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.getAccessTokenLifetime().then(function(data) { + data.should.be.an.instanceOf(Date); + }); + }); + }); + + describe('getRefreshTokenLifetime()', function() { + it('should return a date', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.getRefreshTokenLifetime().then(function(data) { + data.should.be.an.instanceOf(Date); + }); + }); + }); + + describe('getClient()', function() { + 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 return a client', function() { + var client = { id: 12345 }; + 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); + }); + }); + + it('should support promises', function() { + var model = { + getClient: function() { + return Promise.resolve({}); + }, + 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 {}; + }, + 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 the client credentials are 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.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() {}, + 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: {} }); + + return handler.handleGrantType(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing 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: {} }); + + return handler.handleGrantType(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(UnsupportedGrantTypeError); + e.message.should.equal('Unsupported grant type: `grant_type` is invalid'); + }); + }); + + it('should return a grant type result', function() { + var user = {}; + var model = { + getClient: function() {}, + getUser: function() { + return user; + }, + 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).then(function(data) { + data.should.equal(user); + }); + }); + }); + + describe('saveToken()', function() { + it('should return a token', function() { + var token = {}; + var model = { + getClient: function() {}, + saveToken: function() { + return token; + } + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').then(function(data) { + data.should.equal(token); + }); + }); + + it('should support promises', function() { + var model = { + getClient: function() {}, + saveToken: function() { + return Promise.resolve({}); + } + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var model = { + getClient: function() {}, + saveToken: function() { + return {}; + } + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').should.be.an.instanceOf(Promise); + }); + }); + + describe('handleGrantType()', function() { + describe('with grant_type `authorization_code`', function() { + it('should return a user', function() { + var authCode = { client: {}, user: {} }; + var client = {}; + var model = { + getAuthCode: function() { + return authCode; + }, + getClient: function() {}, + saveToken: function() {} + }; + 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(authCode); + }); + }); + }); + + describe('with grant_type `client_credentials`', function() { + it('should return a user', function() { + var user = {}; + var model = { + getClient: function() {}, + getUserFromClient: function() { + return user; + }, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ + body: { + grant_type: 'client_credentials' + }, + headers: {}, + method: {}, + query: {} + }); + + return handler.handleGrantType(request, {}) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + }); + + describe('with grant_type `password`', function() { + it('should return a user', function() { + var user = {}; + var model = { + getClient: function() {}, + getUser: function() { + return user; + }, + 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: {}, + method: {}, + query: {} + }); + + return handler.handleGrantType(request).then(function(data) { + data.should.equal(user); + }); + }); + }); + + describe('with grant_type `refresh_token`', function() { + it('should return a user', function() { + var client = {}; + var refreshToken = { client: {}, user: {} }; + var model = { + getClient: function() {}, + getRefreshToken: function() { + return refreshToken; + }, + saveToken: function() {} + }; + 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(refreshToken); + }); + }); + }); + }); + + describe('getUser()', function() { + describe('with grant_type `authorization_code`', function() { + it('should return a user', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { grant_type: 'authorization_code' }, headers: {}, method: {}, query: {} }); + var user = {}; + + return handler.getUser(request, { user: user }).then(function(data) { + data.should.equal(user); + }); + }); + }); + + describe('with grant_type `client_credentials`', function() { + it('should return a user', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { grant_type: 'client_credentials' }, headers: {}, method: {}, query: {} }); + var result = {}; + + return handler.getUser(request, result).then(function(data) { + data.should.equal(result); + }); + }); + }); + + describe('with grant_type `password`', function() { + it('should return a user', function() { + 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: {} }); + var result = {}; + + return handler.getUser(request, result).then(function(data) { + data.should.equal(result); + }); + }); + }); + + describe('with grant_type `refresh_token`', function() { + it('should return a user', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { grant_type: 'refresh_token' }, headers: {}, method: {}, query: {} }); + var user = {}; + + return handler.getUser(request, { user: user }).then(function(data) { + data.should.equal(user); + }); + }); + }); + }); + + 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' }); + + tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: 120, refreshToken: 'bar' }); + }); + }); + + 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: {} }); + + return handler.updateSuccessResponse(response, tokenType).then(function() { + 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: {} }); + + return handler.updateSuccessResponse(response, tokenType).then(function() { + response.headers.should.containEql({ 'Cache-Control': '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: {} }); + + return handler.updateSuccessResponse(response, tokenType).then(function() { + response.headers.should.containEql({ 'Pragma': '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: {} }); + + return handler.updateErrorResponse(response, error).then(function() { + 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: {} }); + + return handler.updateErrorResponse(response, error).then(function() { + response.status.should.equal(400); + }); + }); + }); +}); diff --git a/test/integration/request_test.js b/test/integration/request_test.js new file mode 100644 index 000000000..25fe4de2e --- /dev/null +++ b/test/integration/request_test.js @@ -0,0 +1,158 @@ + +/** + * Module dependencies. + */ + +var Request = require('../../lib/request'); +var InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); +var should = require('should'); + +/** + * Test `Request`. + */ + +describe('Request', 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: 'bar', method: {}, query: {} }); + + request.headers.should.equal('bar'); + }); + + 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('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; + }); + }); + + 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.be.equal('text/html; charset=utf-8'); + }); + }); +}); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js new file mode 100644 index 000000000..524703363 --- /dev/null +++ b/test/integration/response-types/code-response-type_test.js @@ -0,0 +1,63 @@ + +/** + * Module dependencies. + */ + +var CodeResponseType = require('../../../lib/response-types/code-response-type'); +var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +var should = require('should'); +var url = require('url'); + +/** + * Test `CodeResponseType`. + */ + +describe('CodeResponseType', function() { + describe('constructor()', function() { + it('should throw an error if `code` is missing', function() { + try { + new CodeResponseType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `code`'); + } + }); + + it('should set the `code`', function() { + var responseType = new CodeResponseType('foo'); + + responseType.code.should.equal('foo'); + }); + }); + + describe('getRedirectUri()', function() { + it('should throw an error if the `redirectUri` is missing', function() { + var responseType = new CodeResponseType('foo'); + + try { + responseType.getRedirectUri(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `redirectUri`'); + } + }); + + it('should return the new redirect uri and set the `code` and `state` in the query', function() { + var responseType = new CodeResponseType('foo'); + var redirectUri = responseType.getRedirectUri('http://example.com/cb'); + + url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); + }); + + it('should return the new redirect uri and append the `code` and `state` in the query', function() { + var responseType = new CodeResponseType('foo'); + var redirectUri = responseType.getRedirectUri('http://example.com/cb?foo=bar'); + + url.format(redirectUri).should.equal('http://example.com/cb?foo=bar&code=foo'); + }); + }); +}); diff --git a/test/integration/response_test.js b/test/integration/response_test.js new file mode 100644 index 000000000..2eedff60e --- /dev/null +++ b/test/integration/response_test.js @@ -0,0 +1,60 @@ + +/** + * Module dependencies. + */ + +var Response = require('../../lib/response'); + +/** + * Test `Response`. + */ + +describe('Response', 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: 'bar' }); + + response.headers.should.equal('bar'); + }); + + it('should set the `status` to 200', function() { + var response = new Response({ body: {}, headers: {} }); + + response.status.should.equal(200); + }); + }); + + describe('redirect()', function() { + it('should set the location header to `url`', function() { + var response = new Response({ body: {}, headers: {} }); + + response.redirect('http://example.com'); + + response.headers.should.eql({ Location: '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_test.js b/test/integration/server_test.js new file mode 100644 index 000000000..b762a4999 --- /dev/null +++ b/test/integration/server_test.js @@ -0,0 +1,164 @@ + +/** + * 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`. + */ + +describe('Server', 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); + }); + + it('should set the default `accessTokenLifetime`', function() { + var server = new Server({ model: {} }); + + server.options.accessTokenLifetime.should.equal(3600); + }); + + it('should set the default `authCodeLifetime`', function() { + var server = new Server({ model: {} }); + + server.options.authCodeLifetime.should.equal(300); + }); + + it('should set the default `refreshTokenLifetime`', function() { + var server = new Server({ model: {} }); + + server.options.refreshTokenLifetime.should.equal(1209600); + }); + }); + + describe('authenticate()', function() { + it('should return a promise', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + } + }; + var server = new Server({ model: model }); + var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); + var handler = server.authenticate(request); + + handler.should.be.an.instanceOf(Promise); + }); + + it('should support callbacks', function(next) { + var model = { + getAccessToken: function() { + return { user: {} }; + } + }; + var server = new Server({ model: model }); + var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); + + server.authenticate(request, next); + }); + }); + + describe('authorize()', function() { + it('should return a promise', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + }, + getClient: function() { + return { redirectUri: 'http://example.com/cb' }; + }, + saveAuthCode: function() { + return { authCode: 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: {} }; + }, + getClient: function() { + return { redirectUri: 'http://example.com/cb' }; + }, + saveAuthCode: function() { + return { authCode: 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, next); + }); + }); + + describe('token()', function() { + it('should return a promise', function() { + var model = { + getClient: function() { + return {}; + }, + getUser: function() { + return {}; + }, + saveToken: function() { + return { accessToken: 1234 }; + } + }; + 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 {}; + }, + getUser: function() { + return {}; + }, + saveToken: function() { + return { accessToken: 1234 }; + } + }; + 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: {} }); + + server.token(request, response, next); + }); + }); +}); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js new file mode 100644 index 000000000..b52e705ea --- /dev/null +++ b/test/integration/token-types/bearer-token-type_test.js @@ -0,0 +1,93 @@ + +/** + * 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`. + */ + +describe('BearerTokenType', 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 throw an error if `accessTokenLifetime` is missing', function() { + try { + new BearerTokenType('foo'); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + 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 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_test.js b/test/integration/utils/token-util_test.js new file mode 100644 index 000000000..5e1df5394 --- /dev/null +++ b/test/integration/utils/token-util_test.js @@ -0,0 +1,20 @@ + +/** + * Module dependencies. + */ + +var TokenUtil = require('../../../lib/utils/token-util'); + +/** + * Test `TokenUtil`. + */ + +describe('TokenUtil', function() { + describe('generateRandomToken()', function() { + it('should return a sha-1 token', function() { + return TokenUtil.generateRandomToken().then(function(token) { + token.should.be.a.sha1; + }); + }); + }); +}); diff --git a/test/lockdown.js b/test/lockdown.js deleted file mode 100644 index 5e95e5870..000000000 --- a/test/lockdown.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var express = require('express'), - bodyParser = require('body-parser'), - request = require('supertest'), - should = require('should'); - -var oauth2server = require('../'); -var Authorise = require('../lib/authorise'); - -var bootstrap = function (oauthConfig) { - var app = express(); - app.oauth = oauth2server(oauthConfig || { - model: {} - }); - - app.use(bodyParser()); - - app.all('/oauth/token', app.oauth.grant); - - app.all('/private', function (req, res, next) { - res.send('Hello'); - }); - - app.all('/public', app.oauth.bypass, function (req, res, next) { - res.send('Hello'); - }); - - - app.oauth.lockdown(app); - - app.use(app.oauth.errorHandler()); - - return app; -}; - -describe('Lockdown pattern', function() { - - it('should substitute grant', function (done) { - var app = bootstrap(); - - request(app) - .get('/oauth/token') - .expect(400, /method must be POST/i, done); - }); - - it('should insert authorise by default', function (done) { - var app = bootstrap(); - - request(app) - .get('/private') - .expect(400, /access token was not found/i, done); - }); - - it('should pass valid request through authorise', function (done) { - var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - callback(token !== 'thom', { access_token: token, expires: null }); - } - } - }); - - request(app) - .get('/private?access_token=thom') - .expect(200, /hello/i, done); - }); - - it('should correctly bypass', function (done) { - var app = bootstrap(); - - request(app) - .get('/public') - .expect(200, /hello/i, done); - }); - - describe('in express 3', function () { - var app, privateAction, publicAction; - - beforeEach(function () { - privateAction = function () {}; - publicAction = function () {}; - - // mock express 3 app - app = { - routes: { get: [] } - }; - - app.oauth = oauth2server({ model: {} }); - app.routes.get.push({ callbacks: [ privateAction ] }); - app.routes.get.push({ callbacks: [ app.oauth.bypass, publicAction ] }) - app.oauth.lockdown(app); - }); - - function mockRequest(authoriseFactory) { - var req = { - get: function () {}, - query: { access_token: { expires: null } } - }; - var next = function () {}; - - app.oauth.model.getAccessToken = function (t, c) { c(null, t); }; - - return authoriseFactory(req, null, next); - } - - it('adds authorise to non-bypassed routes', function () { - var authorise = mockRequest(app.routes.get[0].callbacks[0]); - authorise.should.be.an.instanceOf(Authorise); - }); - - it('runs non-bypassed routes after authorise', function () { - app.routes.get[0].callbacks[1].should.equal(privateAction); - }); - - it('removes oauth.bypass from bypassed routes', function () { - app.routes.get[1].callbacks[0].should.equal(publicAction); - }); - }); -}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 000000000..00ecb38eb --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,4 @@ +--require should +--require test/assertions +--ui bdd +--reporter spec diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js new file mode 100644 index 000000000..3b09ad5e8 --- /dev/null +++ b/test/unit/handlers/authenticate-handler_test.js @@ -0,0 +1,91 @@ + +/** + * Module dependencies. + */ + +var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); +var Request = require('../../../lib/request'); +var sinon = require('sinon'); + +/** + * Test `AuthenticateHandler`. + */ + +describe('AuthenticateHandler', function() { + describe('getToken()', 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.getToken(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.getToken(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.getToken(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'); + }); + }); + }); +}); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js new file mode 100644 index 000000000..9d0785b81 --- /dev/null +++ b/test/unit/handlers/authorize-handler_test.js @@ -0,0 +1,68 @@ + +/** + * Module dependencies. + */ + +var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); +var Request = require('../../../lib/request'); +var sinon = require('sinon'); + +/** + * Test `AuthorizeHandler`. + */ + +describe('AuthorizeHandler', function() { + describe('generateAuthCode()', function() { + it('should call `model.generateAuthCode()`', function() { + var model = { + generateAuthCode: sinon.stub().returns({}), + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + return handler.generateAuthCode().then(function() { + model.generateAuthCode.callCount.should.equal(1); + model.generateAuthCode.firstCall.args.should.have.length(0); + }); + }); + }); + + describe('getClient()', function() { + it('should call `model.getClient()`', function() { + var model = { + getAccessToken: function() {}, + getClient: sinon.stub().returns({ redirectUri: 'http://example.com/cb' }), + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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(1); + model.getClient.firstCall.args[0].should.equal(12345); + }); + }); + }); + + describe('saveAuthCode()', function() { + it('should call `model.saveAuthCode()`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: sinon.stub().returns({}) + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + + return handler.saveAuthCode('foo', 'bar', 'biz', 'baz').then(function() { + model.saveAuthCode.callCount.should.equal(1); + model.saveAuthCode.firstCall.args.should.have.length(3); + model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresOn: 'bar' }); + model.saveAuthCode.firstCall.args[1].should.equal('biz'); + model.saveAuthCode.firstCall.args[2].should.equal('baz'); + }); + }); + }); +}); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js new file mode 100644 index 000000000..642535119 --- /dev/null +++ b/test/unit/handlers/token-handler_test.js @@ -0,0 +1,82 @@ + +/** + * Module dependencies. + */ + +var Request = require('../../../lib/request'); +var TokenHandler = require('../../../lib/handlers/token-handler'); +var sinon = require('sinon'); + +/** + * Test `TokenHandler`. + */ + +describe('TokenHandler', function() { + describe('generateAccessToken()', function() { + it('should call `model.generateAccessToken()`', function() { + var model = { + generateAccessToken: sinon.spy(), + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.generateAccessToken().then(function() { + model.generateAccessToken.callCount.should.equal(1); + model.generateAccessToken.firstCall.args.should.have.length(0); + }); + }); + }); + + describe('generateRefreshToken()', function() { + it('should call `model.generateRefreshToken()`', function() { + var model = { + generateRefreshToken: sinon.spy(), + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.generateRefreshToken().then(function() { + model.generateRefreshToken.callCount.should.equal(1); + model.generateRefreshToken.firstCall.args.should.have.length(0); + }); + }); + }); + + describe('getClient()', function() { + it('should call `model.getClient()`', function() { + var model = { + getClient: sinon.stub().returns({}), + 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'); + }); + }); + }); + + describe('saveToken()', function() { + it('should call `model.saveToken()`', function() { + var model = { + getClient: function() {}, + saveToken: sinon.stub().returns({}) + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz' }); + model.saveToken.firstCall.args[1].should.equal('qux'); + model.saveToken.firstCall.args[2].should.equal('fuz'); + }); + }); + }); +}); diff --git a/test/unit/server_test.js b/test/unit/server_test.js new file mode 100644 index 000000000..2e087c3ee --- /dev/null +++ b/test/unit/server_test.js @@ -0,0 +1,71 @@ + +/** + * Module dependencies. + */ + +var AuthenticateHandler = require('../../lib/handlers/authenticate-handler'); +var AuthorizeHandler = require('../../lib/handlers/authorize-handler'); +var Promise = require('bluebird'); +var Server = require('../../lib/server'); +var TokenHandler = require('../../lib/handlers/token-handler'); +var sinon = require('sinon'); + +/** + * Test `Server`. + */ + +describe('Server', function() { + describe('authenticate()', function() { + it('should call `handle`', function() { + var model = { + getAccessToken: function() {} + }; + var server = new Server({ model: model }); + + sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); + + server.authenticate('foo'); + + AuthenticateHandler.prototype.handle.callCount.should.equal(1); + AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); + AuthenticateHandler.prototype.handle.restore(); + }); + }); + + describe('authorize()', function() { + it('should call `handle`', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: 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(); + }); + }); +}); From 65fa496196ff086ef674c9af6bc103fb6920d79e Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Thu, 19 Mar 2015 00:25:55 +0000 Subject: [PATCH 07/39] Add scope support --- lib/errors/invalid-scope-error.js | 31 +++++ lib/handlers/authenticate-handler.js | 52 ++++++++- lib/handlers/authorize-handler.js | 18 ++- lib/handlers/token-handler.js | 20 +++- lib/server.js | 18 ++- lib/token-types/bearer-token-type.js | 7 +- .../handlers/authenticate-handler_test.js | 109 +++++++++++++++--- .../handlers/authorize-handler_test.js | 16 +++ .../handlers/token-handler_test.js | 21 +++- test/integration/server_test.js | 6 +- .../handlers/authenticate-handler_test.js | 28 +++++ test/unit/handlers/authorize-handler_test.js | 4 +- test/unit/handlers/token-handler_test.js | 4 +- 13 files changed, 287 insertions(+), 47 deletions(-) create mode 100644 lib/errors/invalid-scope-error.js diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js new file mode 100644 index 000000000..57aa826ca --- /dev/null +++ b/lib/errors/invalid-scope-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function InvalidScopeError(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'invalid_scope' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(InvalidScopeError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = InvalidScopeError; diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 218dd8986..ed30c556c 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -5,6 +5,7 @@ var InvalidArgumentError = require('../errors/invalid-argument-error'); var InvalidRequestError = require('../errors/invalid-request-error'); +var InvalidScopeError = require('../errors/invalid-scope-error'); var InvalidTokenError = require('../errors/invalid-token-error'); var Promise = require('bluebird'); var Request = require('../request'); @@ -25,7 +26,14 @@ function AuthenticateHandler(options) { throw new ServerError('Server error: model does not implement `getAccessToken()`'); } + if (options.scope) { + if (!options.model.validateScope) { + throw new ServerError('Server error: model does not implement `validateScope()`'); + } + } + this.model = options.model; + this.scope = options.scope; } /** @@ -39,7 +47,15 @@ AuthenticateHandler.prototype.handle = function(request) { return this.getToken(request) .bind(this) - .then(this.getAccessToken); + .then(this.getAccessToken) + .then(function(accessToken) { + return this.validateAccessToken(accessToken) + .bind(this) + .then(this.validateScope) + .then(function() { + return accessToken; + }); + }); }; /** @@ -126,10 +142,6 @@ AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { throw new InvalidTokenError('Invalid token: access token is invalid'); } - if (accessToken.expires && accessToken.expires < new Date()) { - throw new InvalidTokenError('Invalid token: access token has expired'); - } - if (!accessToken.user) { throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); } @@ -138,6 +150,36 @@ AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { }); }); +/** + * Validate access token. + */ + +AuthenticateHandler.prototype.validateAccessToken = Promise.method(function(accessToken) { + if (accessToken.expires && accessToken.expires < new Date()) { + throw new InvalidTokenError('Invalid token: access token has expired'); + } + + return accessToken; +}); + +/** + * Validate scope. + */ + +AuthenticateHandler.prototype.validateScope = Promise.method(function(accessToken) { + if (!this.scope) { + return; + } + + return Promise.try(this.model.validateScope, [accessToken, this.scope]).then(function(scope) { + if (!scope) { + throw new InvalidScopeError('Invalid scope: scope is invalid'); + } + + return scope; + }); +}); + /** * Export constructor. */ diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 73c7f11ad..f32432b45 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -74,6 +74,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { var fns = [ this.generateAuthCode(), this.getAuthCodeLifetime(), + this.getScope(request), this.getClient(request), this.getUser(request), this.getState(request) @@ -81,8 +82,8 @@ AuthorizeHandler.prototype.handle = function(request, response) { return Promise.all(fns) .bind(this) - .spread(function(authCode, expiresOn, client, user, state) { - return this.saveAuthCode(authCode, expiresOn, client, user) + .spread(function(authCode, expiresOn, scope, client, user, state) { + return this.saveAuthCode(authCode, expiresOn, scope, client, user) .bind(this) .then(function(code) { var responseType = this.getResponseType(request, code); @@ -130,6 +131,14 @@ AuthorizeHandler.prototype.getAuthCodeLifetime = Promise.method(function() { return expires; }); +/** + * Get scope from the request body. + */ + +AuthorizeHandler.prototype.getScope = Promise.method(function(request) { + return request.body.scope; +}); + /** * Get the client from the model. */ @@ -183,10 +192,11 @@ AuthorizeHandler.prototype.getUser = Promise.method(function(request) { * Save auth code. */ -AuthorizeHandler.prototype.saveAuthCode = Promise.method(function(authCode, expiresOn, client, user) { +AuthorizeHandler.prototype.saveAuthCode = Promise.method(function(authCode, expiresOn, scope, client, user) { var code = { authCode: authCode, - expiresOn: expiresOn + expiresOn: expiresOn, + scope: scope }; return this.model.saveAuthCode(code, client, user); diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index d91d08d21..a670f3a88 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -87,19 +87,20 @@ TokenHandler.prototype.handle = function(request, response) { this.generateRefreshToken(), this.getAccessTokenLifetime(), this.getRefreshTokenLifetime(), + this.getScope(request), this.getClient(request) ]; return Promise.all(fns) .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, client) { + .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client) { return this.handleGrantType(request, client) .bind(this) .then(function(instance) { return this.getUser(request, instance); }) .then(function(user) { - return this.saveToken(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, client, user); + return this.saveToken(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client, user); }) .then(function(token) { var tokenType = this.getTokenType(token); @@ -168,6 +169,14 @@ TokenHandler.prototype.getRefreshTokenLifetime = Promise.method(function() { return expires; }); +/** + * Get scope from the request body. + */ + +TokenHandler.prototype.getScope = Promise.method(function(request) { + return request.body.scope; +}); + /** * Get the client from the model. */ @@ -249,12 +258,13 @@ TokenHandler.prototype.getUser = Promise.method(function(request, instance) { * Save token. */ -TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, client, user) { +TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client, user) { var token = { accessToken: accessToken, accessTokenExpiresOn: accessTokenExpiresOn, refreshToken: refreshToken, - refreshTokenExpiresOn: refreshTokenExpiresOn + refreshTokenExpiresOn: refreshTokenExpiresOn, + scope: scope }; return this.model.saveToken(token, client, user); @@ -265,7 +275,7 @@ TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshT */ TokenHandler.prototype.getTokenType = function(token) { - return new BearerTokenType(token.accessToken, this.accessTokenLifetime, token.refreshToken); + return new BearerTokenType(token.accessToken, this.accessTokenLifetime, token.refreshToken, token.scope); }; /** diff --git a/lib/server.js b/lib/server.js index 3376bc617..cd2bd3fef 100644 --- a/lib/server.js +++ b/lib/server.js @@ -31,8 +31,10 @@ function OAuth2Server(options) { * Authenticate a token. */ -OAuth2Server.prototype.authenticate = function(request, callback) { - return new AuthenticateHandler(this.options) +OAuth2Server.prototype.authenticate = function(request, options, callback) { + options = _.assign({}, this.options, options); + + return new AuthenticateHandler(options) .handle(request) .nodeify(callback); }; @@ -41,8 +43,10 @@ OAuth2Server.prototype.authenticate = function(request, callback) { * Authorize a request. */ -OAuth2Server.prototype.authorize = function(request, response, callback) { - return new AuthorizeHandler(this.options) +OAuth2Server.prototype.authorize = function(request, response, options, callback) { + options = _.assign({}, this.options, options); + + return new AuthorizeHandler(options) .handle(request, response) .nodeify(callback); }; @@ -51,8 +55,10 @@ OAuth2Server.prototype.authorize = function(request, response, callback) { * Create a token. */ -OAuth2Server.prototype.token = function(request, response, callback) { - return new TokenHandler(this.options) +OAuth2Server.prototype.token = function(request, response, options, callback) { + options = _.assign({}, this.options, options); + + return new TokenHandler(options) .handle(request, response) .nodeify(callback); }; diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index 5b9d16fc3..5d9b19d72 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -9,7 +9,7 @@ var InvalidArgumentError = require('../errors/invalid-argument-error'); * Constructor. */ -function BearerTokenType(accessToken, accessTokenLifetime, refreshToken) { +function BearerTokenType(accessToken, accessTokenLifetime, refreshToken, scope) { if (!accessToken) { throw new InvalidArgumentError('Missing parameter: `accessToken`'); } @@ -21,6 +21,7 @@ function BearerTokenType(accessToken, accessTokenLifetime, refreshToken) { this.accessToken = accessToken; this.accessTokenLifetime = accessTokenLifetime; this.refreshToken = refreshToken; + this.scope = scope; } /** @@ -41,6 +42,10 @@ BearerTokenType.prototype.valueOf = function() { object.refresh_token = this.refreshToken; } + if (this.scope) { + object.scope = this.scope; + } + return object; }; diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index a075c742f..58648a422 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -6,6 +6,7 @@ var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +var InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); var InvalidTokenError = require('../../../lib/errors/invalid-token-error'); var Promise = require('bluebird'); var Request = require('../../../lib/request'); @@ -57,6 +58,16 @@ describe('AuthenticateHandler', function() { grantType.model.should.equal(model); }); + + it('should set the `scope`', function() { + var model = { + getAccessToken: function() {}, + validateScope: function() {} + }; + var grantType = new AuthenticateHandler({ model: model, scope: 'foobar' }); + + grantType.scope.should.equal('foobar'); + }); }); describe('handle()', function() { @@ -78,9 +89,12 @@ describe('AuthenticateHandler', function() { var model = { getAccessToken: function() { return accessToken; + }, + validateScope: function() { + return true; } }; - var handler = new AuthenticateHandler({ model: model }); + var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, @@ -238,21 +252,6 @@ describe('AuthenticateHandler', function() { }); }); - it('should throw an error if `accessToken` is expired', function() { - var accessToken = { expires: new Date() / 10 }; - var model = { - getAccessToken: function() { - return accessToken; - } - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo').catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token has expired'); - }); - }); - it('should throw an error if `accessToken.user` is missing', function() { var model = { getAccessToken: function() { @@ -305,4 +304,82 @@ describe('AuthenticateHandler', function() { handler.getAccessToken('foo').should.be.an.instanceOf(Promise); }); }); + + describe('validateAccessToken()', function() { + it('should throw an error if `accessToken` is expired', function() { + var accessToken = { expires: new Date() / 10 }; + var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + + return handler.validateAccessToken(accessToken) + .then(should.fail) + .catch(function(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: {} }; + var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + + return handler.validateAccessToken(accessToken).then(function(data) { + data.should.equal(accessToken); + }); + }); + + it('should support promises', function() { + var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + + handler.validateAccessToken('foo').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); + + handler.validateAccessToken('foo').should.be.an.instanceOf(Promise); + }); + }); + + describe('validateScope()', function() { + it('should throw an error if `scope` is invalid', function() { + var model = { + getAccessToken: function() {}, + validateScope: function() { + return false; + } + }; + var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + + return handler.validateScope('foo') + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: scope is invalid'); + }); + }); + + it('should support promises', function() { + var model = { + getAccessToken: function() {}, + validateScope: function() { + return true; + } + }; + var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + + handler.validateScope('foo').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var model = { + getAccessToken: function() {}, + validateScope: function() { + return true; + } + }; + var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + + handler.validateScope('foo').should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 9c9e2540b..fc794f0d4 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -342,6 +342,22 @@ describe('AuthorizeHandler', function() { }); }); + describe('getScope()', function() { + it('should return the scope', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request).then(function(scope) { + scope.should.equal('foo'); + }); + }); + }); + describe('getClient()', function() { it('should throw an error if `client_id` is missing', function() { var model = { diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index dcea227cb..6f7113c9e 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -284,7 +284,7 @@ describe('TokenHandler', function() { }); it('should return a bearer token if successful', function() { - var token = { accessToken: 'foo', refreshToken: 'bar', accessTokenLifetime: 120 }; + var token = { accessToken: 'foo', refreshToken: 'bar', accessTokenLifetime: 120, scope: 'foobar' }; var model = { getClient: function() { return {}; @@ -399,6 +399,21 @@ describe('TokenHandler', function() { }); }); + describe('getScope()', function() { + it('should return the scope', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request).then(function(scope) { + scope.should.equal('foo'); + }); + }); + }); + describe('getClient()', function() { it('should throw an error if `client` is missing', function() { var model = { @@ -795,9 +810,9 @@ describe('TokenHandler', function() { saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar' }); + var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); - tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: 120, refreshToken: 'bar' }); + tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: 120, refreshToken: 'bar', scope: 'foobar' }); }); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index b762a4999..a3fb16814 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -76,7 +76,7 @@ describe('Server', function() { var server = new Server({ model: model }); var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - server.authenticate(request, next); + server.authenticate(request, null, next); }); }); @@ -117,7 +117,7 @@ describe('Server', function() { 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, next); + server.authorize(request, response, null, next); }); }); @@ -158,7 +158,7 @@ describe('Server', function() { 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: {} }); - server.token(request, response, next); + server.token(request, response, null, next); }); }); }); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index 3b09ad5e8..7d95103e8 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -88,4 +88,32 @@ describe('AuthenticateHandler', function() { }); }); }); + + describe('validateScope()', function() { + it('should not call `validateScope` if scope is not defined', function() { + var model = { + getAccessToken: function() {}, + validateScope: sinon.stub() + }; + var handler = new AuthenticateHandler({ model: model }); + + return handler.validateScope('foo').then(function() { + model.validateScope.callCount.should.equal(0); + }); + }); + + it('should call `model.getAccessToken()` if scope is defined', function() { + var model = { + getAccessToken: function() {}, + validateScope: sinon.stub().returns(true) + }; + var handler = new AuthenticateHandler({ model: model, scope: 'bar' }); + + return handler.validateScope('foo').then(function() { + model.validateScope.callCount.should.equal(1); + model.validateScope.firstCall.args.should.have.length(2); + model.validateScope.firstCall.args[0].should.equal('foo', 'bar'); + }); + }); + }); }); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 9d0785b81..efc93449a 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -56,10 +56,10 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - return handler.saveAuthCode('foo', 'bar', 'biz', 'baz').then(function() { + return handler.saveAuthCode('foo', 'bar', 'qux', 'biz', 'baz').then(function() { model.saveAuthCode.callCount.should.equal(1); model.saveAuthCode.firstCall.args.should.have.length(3); - model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresOn: 'bar' }); + model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresOn: 'bar', scope: 'qux' }); model.saveAuthCode.firstCall.args[1].should.equal('biz'); model.saveAuthCode.firstCall.args[2].should.equal('baz'); }); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index 642535119..dc510e12f 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -70,10 +70,10 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').then(function() { + return handler.saveToken('foo', 'bar', 'biz', 'baz', 'fiz', 'qux', 'fuz').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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'fiz' }); model.saveToken.firstCall.args[1].should.equal('qux'); model.saveToken.firstCall.args[2].should.equal('fuz'); }); From ce34f073c72799f94c773fb2bc4353b94e5b8f55 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 23 Mar 2015 13:15:09 +0000 Subject: [PATCH 08/39] Update version to 3.0.0 --- package.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/package.json b/package.json index acde84d3a..d0eb36771 100644 --- 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": "2.4.1", + "version": "3.0.0", "keywords": [ "oauth", "oauth2" @@ -10,12 +10,6 @@ "name": "Thom Seddon", "email": "thom@seddonmedia.co.uk" }, - "contributors": [ - { - "name": "Thom Seddon", - "email": "thom@seddonmedia.co.uk" - } - ], "main": "index.js", "dependencies": { "basic-auth": "^1.0.0", From 2689d22419d21967a9f3349e98995c51b6ac9209 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 23 Mar 2015 22:37:28 +0000 Subject: [PATCH 09/39] Add programmatic and operational validations --- .../authorization-code-grant-type.js | 11 +- lib/grant-types/password-grant-type.js | 9 + lib/grant-types/refresh-token-grant-type.js | 9 + lib/handlers/authenticate-handler.js | 11 +- lib/handlers/authorize-handler.js | 17 ++ lib/handlers/token-handler.js | 25 ++ lib/request.js | 7 +- lib/response-types/token-response-type.js | 8 +- lib/response.js | 15 +- lib/token-types/mac-token-type.js | 8 +- lib/validator/is.js | 80 +++++ package.json | 4 +- .../authorization-code-grant-type_test.js | 36 ++- .../client-credentials-grant-type_test.js | 24 ++ .../grant-types/password-grant-type_test.js | 32 +- .../refresh-token-grant-type_test.js | 27 +- .../handlers/authenticate-handler_test.js | 34 ++- .../handlers/authorize-handler_test.js | 149 ++++++--- .../handlers/token-handler_test.js | 282 +++++++++++++----- test/integration/request_test.js | 6 +- test/integration/response_test.js | 20 +- test/integration/utils/token-util_test.js | 9 +- .../handlers/authenticate-handler_test.js | 33 +- test/unit/handlers/authorize-handler_test.js | 39 ++- test/unit/handlers/token-handler_test.js | 51 ++-- 25 files changed, 739 insertions(+), 207 deletions(-) create mode 100644 lib/validator/is.js diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index b62fe4ba2..2295a24af 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -8,6 +8,7 @@ var InvalidGrantError = require('../errors/invalid-grant-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var Promise = require('bluebird'); var ServerError = require('../errors/server-error'); +var is = require('../validator/is'); /** * Constructor. @@ -44,6 +45,10 @@ AuthCodeGrantType.prototype.handle = function(request, client) { return Promise.reject(new InvalidRequestError('Missing parameter: `code`')); } + if (!is.vschar(request.body.code)) { + return Promise.reject(new InvalidRequestError('Invalid parameter: `code`')); + } + return Promise.try(this.model.getAuthCode, request.body.code) .then(function(authCode) { if (!authCode) { @@ -62,7 +67,11 @@ AuthCodeGrantType.prototype.handle = function(request, client) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (authCode.expires && authCode.expires < new Date()) { + if (authCode.expiresOn && !(authCode.expiresOn instanceof Date)) { + throw new ServerError('Server error: `expires` must be a Date instance'); + } + + if (authCode.expiresOn && authCode.expiresOn < new Date()) { throw new InvalidGrantError('Invalid grant: authorization code has expired'); } diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index 3c89b1c63..4b3b5c49c 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -8,6 +8,7 @@ var InvalidGrantError = require('../errors/invalid-grant-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var Promise = require('bluebird'); var ServerError = require('../errors/server-error'); +var is = require('../validator/is'); /** * Constructor. @@ -44,6 +45,14 @@ PasswordGrantType.prototype.handle = function(request) { return Promise.reject(new InvalidRequestError('Missing parameter: `password`')); } + if (!is.uchar(request.body.username)) { + return Promise.reject(new InvalidRequestError('Invalid parameter: `username`')); + } + + if (!is.uchar(request.body.password)) { + return Promise.reject(new InvalidRequestError('Invalid parameter: `password`')); + } + return Promise.try(this.model.getUser, [request.body.username, request.body.password]) .then(function(user) { if (!user) { diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index bf7585602..909b41f2b 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -8,6 +8,7 @@ var InvalidGrantError = require('../errors/invalid-grant-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var Promise = require('bluebird'); var ServerError = require('../errors/server-error'); +var is = require('../validator/is'); /** * Constructor. @@ -44,6 +45,10 @@ RefreshTokenGrantType.prototype.handle = function(request, client) { return Promise.reject(new InvalidRequestError('Missing parameter: `refresh_token`')); } + if (!is.vschar(request.body.refresh_token)) { + return Promise.reject(new InvalidRequestError('Invalid parameter: `refresh_token`')); + } + return Promise.try(this.model.getRefreshToken, request.body.refresh_token) .then(function(refreshToken) { if (!refreshToken) { @@ -62,6 +67,10 @@ RefreshTokenGrantType.prototype.handle = function(request, client) { throw new InvalidGrantError('Invalid grant: refresh token is invalid'); } + if (refreshToken.expires && !(refreshToken.expires instanceof Date)) { + throw new ServerError('Server error: `expires` must be a Date instance'); + } + if (refreshToken.expires && refreshToken.expires < new Date()) { throw new InvalidGrantError('Invalid grant: refresh token has expired'); } diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index ed30c556c..988a963e3 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -10,6 +10,7 @@ var InvalidTokenError = require('../errors/invalid-token-error'); var Promise = require('bluebird'); var Request = require('../request'); var ServerError = require('../errors/server-error'); +var is = require('../validator/is'); /** * Constructor. @@ -26,10 +27,8 @@ function AuthenticateHandler(options) { throw new ServerError('Server error: model does not implement `getAccessToken()`'); } - if (options.scope) { - if (!options.model.validateScope) { - throw new ServerError('Server error: model does not implement `validateScope()`'); - } + if (options.scope && !options.model.validateScope) { + throw new ServerError('Server error: model does not implement `validateScope()`'); } this.model = options.model; @@ -155,6 +154,10 @@ AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { */ AuthenticateHandler.prototype.validateAccessToken = Promise.method(function(accessToken) { + if (accessToken.expires && !(accessToken.expires instanceof Date)) { + throw new ServerError('Server error: `expires` must be a Date instance'); + } + if (accessToken.expires && accessToken.expires < new Date()) { throw new InvalidTokenError('Invalid token: access token has expired'); } diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index f32432b45..b99a5b7d1 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -14,6 +14,7 @@ var Promise = require('bluebird'); var Request = require('../request'); var Response = require('../response'); var ServerError = require('../errors/server-error'); +var is = require('../validator/is'); var tokenUtil = require('../utils/token-util'); var url = require('url'); @@ -136,6 +137,10 @@ AuthorizeHandler.prototype.getAuthCodeLifetime = Promise.method(function() { */ AuthorizeHandler.prototype.getScope = Promise.method(function(request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + return request.body.scope; }); @@ -150,6 +155,10 @@ AuthorizeHandler.prototype.getClient = Promise.method(function(request) { throw new InvalidRequestError('Missing parameter: `client_id`'); } + if (!is.vschar(clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + return Promise.try(this.model.getClient, clientId) .then(function(client) { if (!client) { @@ -160,6 +169,10 @@ AuthorizeHandler.prototype.getClient = Promise.method(function(request) { throw new InvalidClientError('Invalid client: missing client `redirectUri`'); } + if (!is.uri(client.redirectUri)) { + throw new InvalidClientError('Invalid client: `redirectUri` is not a valid URI'); + } + return client; }); }); @@ -175,6 +188,10 @@ AuthorizeHandler.prototype.getState = Promise.method(function(request) { throw new InvalidRequestError('Missing parameter: `state`'); } + if (!is.vschar(state)) { + throw new InvalidRequestError('Invalid parameter: `state`'); + } + return state; }); diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index a670f3a88..4c849f0db 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -15,6 +15,7 @@ var Response = require('../response'); var ServerError = require('../errors/server-error'); var UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error'); var auth = require('basic-auth'); +var is = require('../validator/is'); var tokenUtil = require('../utils/token-util'); /** @@ -174,6 +175,10 @@ TokenHandler.prototype.getRefreshTokenLifetime = Promise.method(function() { */ TokenHandler.prototype.getScope = Promise.method(function(request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + return request.body.scope; }); @@ -184,6 +189,22 @@ TokenHandler.prototype.getScope = Promise.method(function(request) { TokenHandler.prototype.getClient = Promise.method(function(request) { 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(this.model.getClient, [credentials.clientId, credentials.clientSecret]) .then(function(client) { if (!client) { @@ -228,6 +249,10 @@ TokenHandler.prototype.handleGrantType = Promise.method(function(request, client 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'); } diff --git a/lib/request.js b/lib/request.js index 51623c38f..04d4c67c6 100644 --- a/lib/request.js +++ b/lib/request.js @@ -26,9 +26,14 @@ function Request(options) { } this.body = options.body || {}; - this.headers = options.headers || []; + this.headers = {}; this.method = options.method; this.query = options.query; + + // Store the headers in lower case. + for (var field in options.headers) { + this.headers[field.toLowerCase()] = options.headers[field]; + } } /** diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js index e45644452..cb3f11627 100644 --- a/lib/response-types/token-response-type.js +++ b/lib/response-types/token-response-type.js @@ -1,10 +1,16 @@ +/** + * Module dependencies. + */ + +var ServerError = require('../errors/server-error'); + /** * Constructor. */ function TokenResponseType() { - throw new Error('Not implemented.'); + throw new ServerError('Not implemented.'); } /** diff --git a/lib/response.js b/lib/response.js index 67832d31d..411aaf2bd 100644 --- a/lib/response.js +++ b/lib/response.js @@ -7,8 +7,13 @@ function Response(options) { options = options || {}; this.body = options.body || {}; - this.headers = options.headers || []; + this.headers = {}; this.status = 200; + + // Store the headers in lower case. + for (var field in options.headers) { + this.headers[field.toLowerCase()] = options.headers[field]; + } } /** @@ -20,6 +25,14 @@ Response.prototype.redirect = function(url) { this.status = 302; }; +/** + * Get a response header. + */ + +Response.prototype.get = function(field) { + return this.headers[field.toLowerCase()]; +}; + /** * Set a response header. */ diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js index 3a5644e32..07628bbed 100644 --- a/lib/token-types/mac-token-type.js +++ b/lib/token-types/mac-token-type.js @@ -1,10 +1,16 @@ +/** + * Module dependencies. + */ + +var ServerError = require('../errors/server-error'); + /** * Constructor. */ function MacTokenType() { - throw new Error('Not implemented.'); + throw new ServerError('Not implemented.'); } /** diff --git a/lib/validator/is.js b/lib/validator/is.js new file mode 100644 index 000000000..942de6874 --- /dev/null +++ b/lib/validator/is.js @@ -0,0 +1,80 @@ + +/** + * 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|\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/package.json b/package.json index d0eb36771..d444fdb8a 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "dependencies": { "basic-auth": "^1.0.0", "bluebird": "^2.9.13", + "camel-case": "^1.1.1", "lodash": "^3.3.1", "standard-error": "^1.1.0", - "type-is": "^1.6.0" + "type-is": "^1.6.0", + "validator.js": "git+ssh://git@github.com/seegno-forks/validator.js#enhancement/add-extend-method-to-assert" }, "devDependencies": { "mocha": "^2.2.1", diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index b5fdc5576..c5a399343 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -96,6 +96,19 @@ describe('AuthorizationCodeGrantType', function() { }); }); + it('should throw an error if `code` is invalid', function() { + var client = {}; + var grantType = new AuthorizationCodeGrantType({ getAuthCode: function() {} }); + var request = new Request({ body: { code: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code`'); + }); + }); + it('should throw an error if `authCode` is missing', function() { var client = {}; var model = { @@ -170,9 +183,10 @@ describe('AuthorizationCodeGrantType', function() { 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 = { getAuthCode: function() { - return Promise.resolve({ client: { id: 123 }, expires: new Date() / 10, user: {} }); + return Promise.resolve({ client: { id: 123 }, expiresOn: date, user: {} }); } }; var grantType = new AuthorizationCodeGrantType(model); @@ -187,22 +201,24 @@ describe('AuthorizationCodeGrantType', function() { }); it('should return an auth code', function() { - var authCode = { authCode: 12345, client: {}, user: {} }; - var client = {}; + var authCode = { authCode: 12345, client: { id: 'foobar' }, user: {} }; + var client = { id: 'foobar' }; var model = { getAuthCode: sinon.stub().returns(authCode) }; var grantType = new AuthorizationCodeGrantType(model); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client).then(function(data) { - data.should.equal(authCode); - }); + return grantType.handle(request, client) + .then(function(data) { + data.should.equal(authCode); + }) + .catch(should.fail); }); it('should support promises', function() { - var authCode = { authCode: 12345, client: {}, user: {} }; - var client = {}; + var authCode = { authCode: 12345, client: { id: 'foobar' }, user: {} }; + var client = { id: 'foobar' }; var model = { getAuthCode: function() { return Promise.resolve(authCode); @@ -215,8 +231,8 @@ describe('AuthorizationCodeGrantType', function() { }); it('should support non-promises', function() { - var authCode = { authCode: 12345, client: {}, user: {} }; - var client = {}; + var authCode = { authCode: 12345, client: { id: 'foobar' }, user: {} }; + var client = { id: 'foobar' }; var model = { getAuthCode: function() { return authCode; diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 518f7a257..50fa5c077 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -78,6 +78,30 @@ describe('ClientCredentialsGrantType', function() { } }); + it('should throw an error if `client_id` is invalid', function() { + var grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); + var request = new Request({ body: { client_id: 'ø倣‰', client_secret: 'foobar' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + }); + }); + + it('should throw an error if `client_secret` is invalid', function() { + var grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); + var request = new Request({ body: { client_id: 'foobar', client_secret: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + }); + }); + it('should throw an error if `user` is missing', function() { var model = { getUserFromClient: function() {} diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index e81b60a26..af7d377da 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -87,6 +87,30 @@ describe('PasswordGrantType', function() { }); }); + it('should throw an error if `username` is invalid', function() { + var grantType = new PasswordGrantType({ getUser: function() {} }); + var request = new Request({ body: { username: 'ø倣‰', password: 'foobar' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request) + .then(should.fail) + .catch(function(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 grantType = new PasswordGrantType({ getUser: function() {} }); + var request = new Request({ body: { username: 'foobar', password: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request) + .then(should.fail) + .catch(function(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() { @@ -112,9 +136,11 @@ describe('PasswordGrantType', function() { var grantType = new PasswordGrantType(model); var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request).then(function(data) { - data.should.equal(user); - }); + return grantType.handle(request) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); }); it('should support promises when calling `model.getUser()`', function() { diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 7778a6ddc..2e6eb3479 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -130,6 +130,19 @@ describe('RefreshTokenGrantType', function() { }); }); + it('should throw an error if `refresh_token` is invalid', function() { + var client = {}; + var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + var request = new Request({ body: { refresh_token: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return grantType.handle(request, client) + .then(should.fail) + .catch(function(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 grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); @@ -145,9 +158,10 @@ describe('RefreshTokenGrantType', function() { it('should throw an error if `refresh_token` is expired', function() { var client = {}; + var date = new Date(new Date() / 2); var model = { getRefreshToken: function() { - return Promise.resolve({ client: {}, expires: new Date() / 10, user: {} }); + return Promise.resolve({ client: {}, expires: date, user: {} }); } }; var grantType = new RefreshTokenGrantType(model); @@ -163,16 +177,19 @@ describe('RefreshTokenGrantType', function() { it('should return a refresh token', function() { var client = {}; - var refreshToken = { client: {}, expires: new Date() * 10, user: {} }; + var date = new Date(new Date() * 2); + var refreshToken = { client: {}, expires: date, user: {} }; var model = { getRefreshToken: sinon.stub().returns(refreshToken) }; var grantType = new RefreshTokenGrantType(model); var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client).then(function(data) { - data.should.equal(refreshToken); - }); + return grantType.handle(request, client) + .then(function(data) { + data.should.equal(refreshToken); + }) + .catch(should.fail); }); it('should support promises when calling `model.getRefreshToken()`', function() { diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 58648a422..a48f91685 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -102,9 +102,11 @@ describe('AuthenticateHandler', function() { query: {} }); - return handler.handle(request).then(function(data) { - data.should.equal(accessToken); - }); + return handler.handle(request) + .then(function(data) { + data.should.equal(accessToken); + }) + .catch(should.fail); }); }); @@ -170,9 +172,11 @@ describe('AuthenticateHandler', function() { query: {} }); - handler.getTokenFromRequestHeader(request).then(function(bearerToken) { - bearerToken.should.equal('foo'); - }); + return handler.getTokenFromRequestHeader(request) + .then(function(bearerToken) { + bearerToken.should.equal('foo'); + }) + .catch(should.fail); }); }); @@ -277,9 +281,11 @@ describe('AuthenticateHandler', function() { }; var handler = new AuthenticateHandler({ model: model }); - return handler.getAccessToken('foo').then(function(data) { - data.should.equal(accessToken); - }); + return handler.getAccessToken('foo') + .then(function(data) { + data.should.equal(accessToken); + }) + .catch(should.fail); }); it('should support promises', function() { @@ -307,7 +313,7 @@ describe('AuthenticateHandler', function() { describe('validateAccessToken()', function() { it('should throw an error if `accessToken` is expired', function() { - var accessToken = { expires: new Date() / 10 }; + var accessToken = { expires: new Date(new Date() / 2) }; var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); return handler.validateAccessToken(accessToken) @@ -322,9 +328,11 @@ describe('AuthenticateHandler', function() { var accessToken = { user: {} }; var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - return handler.validateAccessToken(accessToken).then(function(data) { - data.should.equal(accessToken); - }); + return handler.validateAccessToken(accessToken) + .then(function(data) { + data.should.equal(accessToken); + }) + .catch(should.fail); }); it('should support promises', function() { diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index fc794f0d4..4fc85a5f0 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -293,9 +293,11 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - return handler.generateAuthCode().then(function(data) { - data.should.be.a.sha1; - }); + return handler.generateAuthCode() + .then(function(data) { + data.should.be.a.sha1; + }) + .catch(should.fail); }); it('should support promises', function() { @@ -336,13 +338,32 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - return handler.getAuthCodeLifetime().then(function(data) { - data.should.be.an.instanceOf(Date); - }); + return handler.getAuthCodeLifetime() + .then(function(data) { + data.should.be.an.instanceOf(Date); + }) + .catch(should.fail); }); }); describe('getScope()', function() { + it('should throw an error if `scope` is invalid', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + }); + }); + it('should return the scope', function() { var model = { getAccessToken: function() {}, @@ -352,9 +373,11 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request).then(function(scope) { - scope.should.equal('foo'); - }); + return handler.getScope(request) + .then(function(scope) { + scope.should.equal('foo'); + }) + .catch(should.fail); }); }); @@ -376,6 +399,23 @@ describe('AuthorizeHandler', function() { }); }); + it('should throw an error if `client_id` is invalid', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { client_id: 'ø倣‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + }); + }); + it('should throw an error if `client` is missing', function() { var model = { getAccessToken: function() {}, @@ -396,7 +436,7 @@ describe('AuthorizeHandler', function() { it('should throw an error if `client.redirectUri` is missing', function() { var model = { getAccessToken: function() {}, - getClient: function() { return {} }, + getClient: function() { return {}; }, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); @@ -410,6 +450,23 @@ describe('AuthorizeHandler', function() { }); }); + it('should throw an error if `client.redirectUri` is invalid', function() { + var model = { + getAccessToken: function() {}, + getClient: function() { return { redirectUri: 'foobar' }; }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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: `redirectUri` is not a valid URI'); + }); + }); + it('should support promises', function() { var model = { getAccessToken: function() {}, @@ -461,9 +518,11 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - return handler.getClient(request).then(function(data) { - data.should.equal(client); - }); + return handler.getClient(request) + .then(function(data) { + data.should.equal(client); + }) + .catch(should.fail); }); }); @@ -480,9 +539,11 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 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); - }); + return handler.getClient(request) + .then(function(data) { + data.should.equal(client); + }) + .catch(should.fail); }); }); }); @@ -497,10 +558,12 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return handler.getState(request).catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `state`'); - }); + return handler.getState(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + }); }); it('should throw an error if `state` is invalid', function() { @@ -512,13 +575,15 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'ø倣‰' } }); - return handler.getState(request).catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `state`'); - }); + return handler.getState(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `state`'); + }); }); - describe('with `response_type` in the request body', function() { + describe('with `state` in the request body', function() { it('should return the state', function() { var model = { getAccessToken: function() {}, @@ -528,13 +593,15 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); - return handler.getState(request).then(function(data) { - data.should.equal('foobar'); - }); + return handler.getState(request) + .then(function(data) { + data.should.equal('foobar'); + }) + .catch(should.fail); }); }); - describe('with `response_type` in the request query', function() { + describe('with `state` in the request query', function() { it('should return the state', function() { var model = { getAccessToken: function() {}, @@ -544,9 +611,11 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); - return handler.getState(request).then(function(data) { - data.should.equal('foobar'); - }); + return handler.getState(request) + .then(function(data) { + data.should.equal('foobar'); + }) + .catch(should.fail); }); }); }); @@ -564,9 +633,11 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - return handler.getUser(request).then(function(data) { - data.should.equal(user); - }); + return handler.getUser(request) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); }); }); @@ -582,9 +653,11 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - return handler.saveAuthCode('foo', 'bar', 'biz', 'baz').then(function(data) { - data.should.equal(authCode); - }); + return handler.saveAuthCode('foo', 'bar', 'biz', 'baz') + .then(function(data) { + data.should.equal(authCode); + }) + .catch(should.fail); }); it('should support promises when calling `model.saveAuthCode()`', function() { diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 6f7113c9e..7960146c7 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -8,6 +8,7 @@ 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 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'); @@ -311,9 +312,11 @@ describe('TokenHandler', function() { }); var response = new Response({ body: {}, headers: {} }); - return handler.handle(request, response).then(function(data) { - data.should.eql(token); - }); + return handler.handle(request, response) + .then(function(data) { + data.should.eql(token); + }) + .catch(should.fail); }); }); @@ -325,9 +328,11 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.generateAccessToken().then(function(data) { - data.should.be.a.sha1; - }); + return handler.generateAccessToken() + .then(function(data) { + data.should.be.a.sha1; + }) + .catch(should.fail); }); it('should support promises', function() { @@ -365,9 +370,11 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.generateRefreshToken().then(function(data) { - data.should.be.a.sha1; - }); + return handler.generateRefreshToken() + .then(function(data) { + data.should.be.a.sha1; + }) + .catch(should.fail); }); }); @@ -379,9 +386,11 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.getAccessTokenLifetime().then(function(data) { - data.should.be.an.instanceOf(Date); - }); + return handler.getAccessTokenLifetime() + .then(function(data) { + data.should.be.an.instanceOf(Date); + }) + .catch(should.fail); }); }); @@ -393,13 +402,31 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.getRefreshTokenLifetime().then(function(data) { - data.should.be.an.instanceOf(Date); - }); + return handler.getRefreshTokenLifetime() + .then(function(data) { + data.should.be.an.instanceOf(Date); + }) + .catch(should.fail); }); }); describe('getScope()', function() { + it('should throw an error if `scope` is invalid', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + }); + }); + it('should return the scope', function() { var model = { getClient: function() {}, @@ -408,13 +435,47 @@ describe('TokenHandler', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request).then(function(scope) { - scope.should.equal('foo'); - }); + return handler.getScope(request) + .then(function(scope) { + scope.should.equal('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: {} }); + + return handler.getClient(request) + .then(should.fail) + .catch(function(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() {}, + 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: {} }); + + return handler.getClient(request) + .then(should.fail) + .catch(function(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() {}, @@ -442,9 +503,11 @@ describe('TokenHandler', 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); - }); + return handler.getClient(request) + .then(function(data) { + data.should.equal(client); + }) + .catch(should.fail); }); it('should support promises', function() { @@ -475,13 +538,31 @@ describe('TokenHandler', function() { }); describe('getClientCredentials()', function() { - it('should throw an error if the client credentials are missing', 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: {}, headers: {}, method: {}, query: {} }); + 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); @@ -546,6 +627,22 @@ describe('TokenHandler', function() { }); }); + 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: {} }); + + return handler.handleGrantType(request) + .then(should.fail) + .catch(function(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() {}, @@ -562,7 +659,26 @@ describe('TokenHandler', function() { }); }); - it('should return a grant type result', function() { + it('should return a grant type result if the `grant_type` is a uri', function() { + var user = {}; + var model = { + getClient: function() {}, + getUser: function() { + return user; + }, + saveToken: function() {} + }; + 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) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + + it('should return a grant type result if the `grant_type` is not a uri', function() { var user = {}; var model = { getClient: function() {}, @@ -574,9 +690,11 @@ describe('TokenHandler', 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).then(function(data) { - data.should.equal(user); - }); + return handler.handleGrantType(request) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); }); }); @@ -591,9 +709,11 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').then(function(data) { - data.should.equal(token); - }); + return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz') + .then(function(data) { + data.should.equal(token); + }) + .catch(should.fail); }); it('should support promises', function() { @@ -624,8 +744,8 @@ describe('TokenHandler', function() { describe('handleGrantType()', function() { describe('with grant_type `authorization_code`', function() { it('should return a user', function() { - var authCode = { client: {}, user: {} }; - var client = {}; + var authCode = { client: { id: 'foobar' }, user: {} }; + var client = { id: 'foobar' }; var model = { getAuthCode: function() { return authCode; @@ -644,9 +764,11 @@ describe('TokenHandler', function() { query: {} }); - return handler.handleGrantType(request, client).then(function(data) { - data.should.equal(authCode); - }); + return handler.handleGrantType(request, client) + .then(function(data) { + data.should.equal(authCode); + }) + .catch(should.fail); }); }); @@ -702,9 +824,11 @@ describe('TokenHandler', function() { query: {} }); - return handler.handleGrantType(request).then(function(data) { - data.should.equal(user); - }); + return handler.handleGrantType(request) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); }); }); @@ -730,9 +854,11 @@ describe('TokenHandler', function() { query: {} }); - return handler.handleGrantType(request, client).then(function(data) { - data.should.equal(refreshToken); - }); + return handler.handleGrantType(request, client) + .then(function(data) { + data.should.equal(refreshToken); + }) + .catch(should.fail); }); }); }); @@ -748,9 +874,11 @@ describe('TokenHandler', function() { var request = new Request({ body: { grant_type: 'authorization_code' }, headers: {}, method: {}, query: {} }); var user = {}; - return handler.getUser(request, { user: user }).then(function(data) { - data.should.equal(user); - }); + return handler.getUser(request, { user: user }) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); }); }); @@ -764,9 +892,11 @@ describe('TokenHandler', function() { var request = new Request({ body: { grant_type: 'client_credentials' }, headers: {}, method: {}, query: {} }); var result = {}; - return handler.getUser(request, result).then(function(data) { - data.should.equal(result); - }); + return handler.getUser(request, result) + .then(function(data) { + data.should.equal(result); + }) + .catch(should.fail); }); }); @@ -780,9 +910,11 @@ describe('TokenHandler', function() { var request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, query: {} }); var result = {}; - return handler.getUser(request, result).then(function(data) { - data.should.equal(result); - }); + return handler.getUser(request, result) + .then(function(data) { + data.should.equal(result); + }) + .catch(should.fail); }); }); @@ -796,9 +928,11 @@ describe('TokenHandler', function() { var request = new Request({ body: { grant_type: 'refresh_token' }, headers: {}, method: {}, query: {} }); var user = {}; - return handler.getUser(request, { user: user }).then(function(data) { - data.should.equal(user); - }); + return handler.getUser(request, { user: user }) + .then(function(data) { + data.should.equal(user); + }) + .catch(should.fail); }); }); }); @@ -826,9 +960,11 @@ describe('TokenHandler', function() { var tokenType = new BearerTokenType('foo', 'bar', 'biz'); var response = new Response({ body: {}, headers: {} }); - return handler.updateSuccessResponse(response, tokenType).then(function() { - response.body.should.eql({ access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', token_type: 'bearer' }); - }); + return handler.updateSuccessResponse(response, tokenType) + .then(function() { + response.body.should.eql({ access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', token_type: 'bearer' }); + }) + .catch(should.fail); }); it('should set the `Cache-Control` header', function() { @@ -840,9 +976,11 @@ describe('TokenHandler', function() { var tokenType = new BearerTokenType('foo', 'bar', 'biz'); var response = new Response({ body: {}, headers: {} }); - return handler.updateSuccessResponse(response, tokenType).then(function() { - response.headers.should.containEql({ 'Cache-Control': 'no-store' }); - }); + return handler.updateSuccessResponse(response, tokenType) + .then(function() { + response.get('Cache-Control').should.equal('no-store'); + }) + .catch(should.fail); }); it('should set the `Pragma` header', function() { @@ -854,9 +992,11 @@ describe('TokenHandler', function() { var tokenType = new BearerTokenType('foo', 'bar', 'biz'); var response = new Response({ body: {}, headers: {} }); - return handler.updateSuccessResponse(response, tokenType).then(function() { - response.headers.should.containEql({ 'Pragma': 'no-cache' }); - }); + return handler.updateSuccessResponse(response, tokenType) + .then(function() { + response.get('Pragma').should.equal('no-cache'); + }) + .catch(should.fail); }); }); @@ -870,10 +1010,12 @@ describe('TokenHandler', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var response = new Response({ body: {}, headers: {} }); - return handler.updateErrorResponse(response, error).then(function() { - response.body.error.should.equal('access_denied'); - response.body.error_description.should.equal('Cannot request a token'); - }); + return handler.updateErrorResponse(response, error) + .then(function() { + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a token'); + }) + .catch(should.fail); }); it('should set the `status`', function() { @@ -885,9 +1027,11 @@ describe('TokenHandler', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var response = new Response({ body: {}, headers: {} }); - return handler.updateErrorResponse(response, error).then(function() { - response.status.should.equal(400); - }); + return handler.updateErrorResponse(response, error) + .then(function() { + response.status.should.equal(400); + }) + .catch(should.fail); }); }); }); diff --git a/test/integration/request_test.js b/test/integration/request_test.js index 25fe4de2e..0e9743f11 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -53,9 +53,9 @@ describe('Request', function() { }); it('should set the `headers`', function() { - var request = new Request({ body: {}, headers: 'bar', method: {}, query: {} }); + var request = new Request({ body: {}, headers: { foo: 'bar', QuX: 'biz' }, method: {}, query: {} }); - request.headers.should.equal('bar'); + request.headers.should.eql({ foo: 'bar', qux: 'biz' }); }); it('should set the `method`', function() { @@ -152,7 +152,7 @@ describe('Request', function() { query: {} }); - request.get('content-type').should.be.equal('text/html; charset=utf-8'); + request.get('Content-Type').should.equal('text/html; charset=utf-8'); }); }); }); diff --git a/test/integration/response_test.js b/test/integration/response_test.js index 2eedff60e..f1cc7b10c 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -18,9 +18,9 @@ describe('Response', function() { }); it('should set the `headers`', function() { - var response = new Response({ body: {}, headers: 'bar' }); + var response = new Response({ body: {}, headers: { foo: 'bar', QuX: 'biz' } }); - response.headers.should.equal('bar'); + response.headers.should.eql({ foo: 'bar', qux: 'biz' }); }); it('should set the `status` to 200', function() { @@ -36,7 +36,7 @@ describe('Response', function() { response.redirect('http://example.com'); - response.headers.should.eql({ Location: 'http://example.com' }); + response.get('Location').should.equal('http://example.com'); }); it('should set the `status` to 302', function() { @@ -48,6 +48,20 @@ describe('Response', function() { }); }); + 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('set()', function() { it('should set the `field`', function() { var response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js index 5e1df5394..e2ac9fc91 100644 --- a/test/integration/utils/token-util_test.js +++ b/test/integration/utils/token-util_test.js @@ -4,6 +4,7 @@ */ var TokenUtil = require('../../../lib/utils/token-util'); +var should = require('should'); /** * Test `TokenUtil`. @@ -12,9 +13,11 @@ var TokenUtil = require('../../../lib/utils/token-util'); describe('TokenUtil', function() { describe('generateRandomToken()', function() { it('should return a sha-1 token', function() { - return TokenUtil.generateRandomToken().then(function(token) { - token.should.be.a.sha1; - }); + return TokenUtil.generateRandomToken() + .then(function(token) { + token.should.be.a.sha1; + }) + .catch(should.fail); }); }); }); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index 7d95103e8..a94ddc0ae 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -6,6 +6,7 @@ var AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); var Request = require('../../../lib/request'); var sinon = require('sinon'); +var should = require('should'); /** * Test `AuthenticateHandler`. @@ -81,11 +82,13 @@ describe('AuthenticateHandler', function() { }; 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'); - }); + 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'); + }) + .catch(should.fail); }); }); @@ -97,9 +100,11 @@ describe('AuthenticateHandler', function() { }; var handler = new AuthenticateHandler({ model: model }); - return handler.validateScope('foo').then(function() { - model.validateScope.callCount.should.equal(0); - }); + return handler.validateScope('foo') + .then(function() { + model.validateScope.callCount.should.equal(0); + }) + .catch(should.fail); }); it('should call `model.getAccessToken()` if scope is defined', function() { @@ -109,11 +114,13 @@ describe('AuthenticateHandler', function() { }; var handler = new AuthenticateHandler({ model: model, scope: 'bar' }); - return handler.validateScope('foo').then(function() { - model.validateScope.callCount.should.equal(1); - model.validateScope.firstCall.args.should.have.length(2); - model.validateScope.firstCall.args[0].should.equal('foo', 'bar'); - }); + return handler.validateScope('foo') + .then(function() { + model.validateScope.callCount.should.equal(1); + model.validateScope.firstCall.args.should.have.length(2); + model.validateScope.firstCall.args[0].should.equal('foo', 'bar'); + }) + .catch(should.fail); }); }); }); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index efc93449a..307c5bd74 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -6,6 +6,7 @@ var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); var Request = require('../../../lib/request'); var sinon = require('sinon'); +var should = require('should'); /** * Test `AuthorizeHandler`. @@ -22,10 +23,12 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - return handler.generateAuthCode().then(function() { - model.generateAuthCode.callCount.should.equal(1); - model.generateAuthCode.firstCall.args.should.have.length(0); - }); + return handler.generateAuthCode() + .then(function() { + model.generateAuthCode.callCount.should.equal(1); + model.generateAuthCode.firstCall.args.should.have.length(0); + }) + .catch(should.fail); }); }); @@ -39,11 +42,13 @@ describe('AuthorizeHandler', function() { var handler = new AuthorizeHandler({ authCodeLifetime: 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(1); - model.getClient.firstCall.args[0].should.equal(12345); - }); + return handler.getClient(request) + .then(function() { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(1); + model.getClient.firstCall.args[0].should.equal(12345); + }) + .catch(should.fail); }); }); @@ -56,13 +61,15 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - return handler.saveAuthCode('foo', 'bar', 'qux', 'biz', 'baz').then(function() { - model.saveAuthCode.callCount.should.equal(1); - model.saveAuthCode.firstCall.args.should.have.length(3); - model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresOn: 'bar', scope: 'qux' }); - model.saveAuthCode.firstCall.args[1].should.equal('biz'); - model.saveAuthCode.firstCall.args[2].should.equal('baz'); - }); + return handler.saveAuthCode('foo', 'bar', 'qux', 'biz', 'baz') + .then(function() { + model.saveAuthCode.callCount.should.equal(1); + model.saveAuthCode.firstCall.args.should.have.length(3); + model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresOn: 'bar', scope: 'qux' }); + model.saveAuthCode.firstCall.args[1].should.equal('biz'); + model.saveAuthCode.firstCall.args[2].should.equal('baz'); + }) + .catch(should.fail); }); }); }); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index dc510e12f..373ccd910 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -6,6 +6,7 @@ var Request = require('../../../lib/request'); var TokenHandler = require('../../../lib/handlers/token-handler'); var sinon = require('sinon'); +var should = require('should'); /** * Test `TokenHandler`. @@ -21,10 +22,12 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.generateAccessToken().then(function() { - model.generateAccessToken.callCount.should.equal(1); - model.generateAccessToken.firstCall.args.should.have.length(0); - }); + return handler.generateAccessToken() + .then(function() { + model.generateAccessToken.callCount.should.equal(1); + model.generateAccessToken.firstCall.args.should.have.length(0); + }) + .catch(should.fail); }); }); @@ -37,10 +40,12 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.generateRefreshToken().then(function() { - model.generateRefreshToken.callCount.should.equal(1); - model.generateRefreshToken.firstCall.args.should.have.length(0); - }); + return handler.generateRefreshToken() + .then(function() { + model.generateRefreshToken.callCount.should.equal(1); + model.generateRefreshToken.firstCall.args.should.have.length(0); + }) + .catch(should.fail); }); }); @@ -53,12 +58,14 @@ describe('TokenHandler', 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'); - }); + 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); }); }); @@ -70,13 +77,15 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.saveToken('foo', 'bar', 'biz', 'baz', 'fiz', 'qux', 'fuz').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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'fiz' }); - model.saveToken.firstCall.args[1].should.equal('qux'); - model.saveToken.firstCall.args[2].should.equal('fuz'); - }); + return handler.saveToken('foo', 'bar', 'biz', 'baz', 'fiz', 'qux', 'fuz') + .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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'fiz' }); + model.saveToken.firstCall.args[1].should.equal('qux'); + model.saveToken.firstCall.args[2].should.equal('fuz'); + }) + .catch(should.fail); }); }); }); From 5ef7346eb9ff077f3b7fb175a7e7bb1d5f79f5cb Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 30 Mar 2015 12:36:47 +0100 Subject: [PATCH 10/39] Improve authorize and token test organization --- lib/errors/unauthorized-client-error.js | 31 +++ lib/handlers/authenticate-handler.js | 5 +- lib/handlers/authorize-handler.js | 55 ++-- lib/handlers/token-handler.js | 153 ++++++----- .../client-credentials-grant-type_test.js | 24 -- .../handlers/authenticate-handler_test.js | 2 +- .../handlers/authorize-handler_test.js | 250 ++++++++++++------ .../handlers/token-handler_test.js | 243 ++++++++++++----- test/integration/server_test.js | 8 +- test/unit/handlers/authorize-handler_test.js | 2 +- test/unit/handlers/token-handler_test.js | 7 +- 11 files changed, 512 insertions(+), 268 deletions(-) create mode 100644 lib/errors/unauthorized-client-error.js diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js new file mode 100644 index 000000000..e56e9e445 --- /dev/null +++ b/lib/errors/unauthorized-client-error.js @@ -0,0 +1,31 @@ + +/** + * Module dependencies. + */ + +var OAuthError = require('./oauth-error'); +var util = require('util'); + +/** + * Constructor. + */ + +function UnauthorizedClientError(message) { + OAuthError.call(this, { + code: 400, + message: message, + name: 'unauthorized_client' + }); +} + +/** + * Inherit prototype. + */ + +util.inherits(UnauthorizedClientError, OAuthError); + +/** + * Export constructor. + */ + +module.exports = UnauthorizedClientError; diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 988a963e3..d2c696f15 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -10,7 +10,6 @@ var InvalidTokenError = require('../errors/invalid-token-error'); var Promise = require('bluebird'); var Request = require('../request'); var ServerError = require('../errors/server-error'); -var is = require('../validator/is'); /** * Constructor. @@ -154,11 +153,11 @@ AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { */ AuthenticateHandler.prototype.validateAccessToken = Promise.method(function(accessToken) { - if (accessToken.expires && !(accessToken.expires instanceof Date)) { + if (accessToken.accessTokenExpiresOn && !(accessToken.accessTokenExpiresOn instanceof Date)) { throw new ServerError('Server error: `expires` must be a Date instance'); } - if (accessToken.expires && accessToken.expires < new Date()) { + if (accessToken.accessTokenExpiresOn && accessToken.accessTokenExpiresOn < new Date()) { throw new InvalidTokenError('Invalid token: access token has expired'); } diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index b99a5b7d1..4f4b3271e 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -14,6 +14,7 @@ var Promise = require('bluebird'); var Request = require('../request'); var Response = require('../response'); var ServerError = require('../errors/server-error'); +var UnauthorizedClientError = require('../errors/unauthorized-client-error'); var is = require('../validator/is'); var tokenUtil = require('../utils/token-util'); var url = require('url'); @@ -75,15 +76,15 @@ AuthorizeHandler.prototype.handle = function(request, response) { var fns = [ this.generateAuthCode(), this.getAuthCodeLifetime(), - this.getScope(request), this.getClient(request), - this.getUser(request), - this.getState(request) + this.getScope(request), + this.getState(request), + this.getUser(request) ]; return Promise.all(fns) .bind(this) - .spread(function(authCode, expiresOn, scope, client, user, state) { + .spread(function(authCode, expiresOn, client, scope, state, user) { return this.saveAuthCode(authCode, expiresOn, scope, client, user) .bind(this) .then(function(code) { @@ -132,18 +133,6 @@ AuthorizeHandler.prototype.getAuthCodeLifetime = Promise.method(function() { return expires; }); -/** - * Get scope from the request body. - */ - -AuthorizeHandler.prototype.getScope = Promise.method(function(request) { - if (!is.nqschar(request.body.scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); - } - - return request.body.scope; -}); - /** * Get the client from the model. */ @@ -159,24 +148,50 @@ AuthorizeHandler.prototype.getClient = Promise.method(function(request) { 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 Promise.try(this.model.getClient, clientId) .then(function(client) { if (!client) { throw new InvalidClientError('Invalid client: client credentials are invalid'); } + if (!client.grants) { + throw new InvalidClientError('Invalid client: missing client `grants`'); + } + + if (!_.contains(client.grants, 'authorization_code')) { + throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); + } + if (!client.redirectUri) { throw new InvalidClientError('Invalid client: missing client `redirectUri`'); } - if (!is.uri(client.redirectUri)) { - throw new InvalidClientError('Invalid client: `redirectUri` is not a valid URI'); + if (redirectUri && client.redirectUri !== redirectUri) { + throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); } return client; }); }); +/** + * Get scope from the request body. + */ + +AuthorizeHandler.prototype.getScope = Promise.method(function(request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + + return request.body.scope; +}); + /** * Get state from the request. */ @@ -270,10 +285,10 @@ AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) */ AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, state) { + redirectUri.query = redirectUri.query || {}; redirectUri.query.state = state; - redirectUri = url.format(redirectUri); - response.redirect(redirectUri); + response.redirect(url.format(redirectUri)); }; /** diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 4c849f0db..accfb94b4 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -13,6 +13,7 @@ var Promise = require('bluebird'); var Request = require('../request'); var Response = require('../response'); var ServerError = require('../errors/server-error'); +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'); @@ -83,42 +84,45 @@ TokenHandler.prototype.handle = function(request, response) { return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')); } - var fns = [ - this.generateAccessToken(), - this.generateRefreshToken(), - this.getAccessTokenLifetime(), - this.getRefreshTokenLifetime(), - this.getScope(request), - this.getClient(request) - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client) { + return Promise.bind(this) + .then(function() { + return this.getClient(request); + }) + .then(function(client) { return this.handleGrantType(request, client) .bind(this) .then(function(instance) { - return this.getUser(request, instance); - }) - .then(function(user) { - return this.saveToken(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client, user); - }) - .then(function(token) { - var tokenType = this.getTokenType(token); - - this.updateSuccessResponse(response, tokenType); - - return token; - }) - .catch(function(e) { - if (!(e instanceof OAuthError)) { - e = new ServerError(e.message); - } - - this.updateErrorResponse(response, e); - - throw e; + var fns = [ + this.generateAccessToken(), + this.generateRefreshToken(client), + this.getAccessTokenExpiresOn(), + this.getRefreshTokenExpiresOn(client), + this.getScope(request), + this.getUser(request, instance) + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, user) { + return this.saveToken(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client, user) + .bind(this) + .then(function(token) { + var tokenType = this.getTokenType(accessToken, refreshToken, scope); + + this.updateSuccessResponse(response, tokenType); + + return token; + }); + }); }); + }).catch(function(e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e.message); + } + + this.updateErrorResponse(response, e); + + throw e; }); }; @@ -138,7 +142,11 @@ TokenHandler.prototype.generateAccessToken = Promise.method(function() { * Generate refresh token. */ -TokenHandler.prototype.generateRefreshToken = Promise.method(function() { +TokenHandler.prototype.generateRefreshToken = Promise.method(function(client) { + if (!_.contains(client.grants, 'refresh_token')) { + return; + } + if (this.model.generateRefreshToken) { return this.model.generateRefreshToken(); } @@ -147,10 +155,10 @@ TokenHandler.prototype.generateRefreshToken = Promise.method(function() { }); /** - * Get access token lifetime. + * Get access token expires on. */ -TokenHandler.prototype.getAccessTokenLifetime = Promise.method(function() { +TokenHandler.prototype.getAccessTokenExpiresOn = Promise.method(function() { var expires = new Date(); expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); @@ -158,30 +166,6 @@ TokenHandler.prototype.getAccessTokenLifetime = Promise.method(function() { return expires; }); -/** - * Get refresh token lifetime. - */ - -TokenHandler.prototype.getRefreshTokenLifetime = Promise.method(function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); - - return expires; -}); - -/** - * Get scope from the request body. - */ - -TokenHandler.prototype.getScope = Promise.method(function(request) { - if (!is.nqschar(request.body.scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); - } - - return request.body.scope; -}); - /** * Get the client from the model. */ @@ -211,10 +195,46 @@ TokenHandler.prototype.getClient = Promise.method(function(request) { throw new InvalidClientError('Invalid client: client is invalid'); } + if (!client.grants) { + throw new InvalidClientError('Invalid client: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new InvalidClientError('Invalid client: `grants` must be an array'); + } + return client; }); }); +/** + * Get refresh token expires on. + */ + +TokenHandler.prototype.getRefreshTokenExpiresOn = Promise.method(function(client) { + if (!_.contains(client.grants, 'refresh_token')) { + return; + } + + var expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); + + return expires; +}); + +/** + * Get scope from the request body. + */ + +TokenHandler.prototype.getScope = Promise.method(function(request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + + return request.body.scope; +}); + /** * Get client credentials. * @@ -257,6 +277,10 @@ TokenHandler.prototype.handleGrantType = Promise.method(function(request, client throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid'); } + if (!_.contains(client.grants, grantType)) { + throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); + } + var Type = this.grantTypes[grantType]; return new Type(this.model) @@ -287,11 +311,14 @@ TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshT var token = { accessToken: accessToken, accessTokenExpiresOn: accessTokenExpiresOn, - refreshToken: refreshToken, - refreshTokenExpiresOn: refreshTokenExpiresOn, scope: scope }; + if (refreshToken) { + token.refreshToken = refreshToken; + token.refreshTokenExpiresOn = refreshTokenExpiresOn; + } + return this.model.saveToken(token, client, user); }); @@ -299,8 +326,8 @@ TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshT * Get token type. */ -TokenHandler.prototype.getTokenType = function(token) { - return new BearerTokenType(token.accessToken, this.accessTokenLifetime, token.refreshToken, token.scope); +TokenHandler.prototype.getTokenType = function(accessToken, refreshToken, scope) { + return new BearerTokenType(accessToken, this.accessTokenLifetime, refreshToken, scope); }; /** diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 50fa5c077..518f7a257 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -78,30 +78,6 @@ describe('ClientCredentialsGrantType', function() { } }); - it('should throw an error if `client_id` is invalid', function() { - var grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); - var request = new Request({ body: { client_id: 'ø倣‰', client_secret: 'foobar' }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - }); - }); - - it('should throw an error if `client_secret` is invalid', function() { - var grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); - var request = new Request({ body: { client_id: 'foobar', client_secret: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - - return grantType.handle(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_secret`'); - }); - }); - it('should throw an error if `user` is missing', function() { var model = { getUserFromClient: function() {} diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index a48f91685..758a328df 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -313,7 +313,7 @@ describe('AuthenticateHandler', function() { describe('validateAccessToken()', function() { it('should throw an error if `accessToken` is expired', function() { - var accessToken = { expires: new Date(new Date() / 2) }; + var accessToken = { accessTokenExpiresOn: new Date(new Date() / 2) }; var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); return handler.validateAccessToken(accessToken) diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 4fc85a5f0..7217b1679 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -14,6 +14,7 @@ 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'); @@ -179,7 +180,7 @@ describe('AuthorizeHandler', function() { return { user: {} }; }, getClient: function() { - return { redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, saveAuthCode: function() { throw new AccessDeniedError('Cannot request this auth code'); @@ -203,13 +204,13 @@ describe('AuthorizeHandler', function() { return handler.handle(request, response) .then(should.fail) - .catch(function(e) { + .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 = { redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; var model = { getAccessToken: function() { return { client: client, user: {} }; @@ -245,7 +246,7 @@ describe('AuthorizeHandler', function() { }); it('should return the `code` if successful', function() { - var client = { redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; var model = { getAccessToken: function() { return { client: client, user: {} }; @@ -277,7 +278,7 @@ describe('AuthorizeHandler', function() { .then(function(data) { data.should.eql({ authCode: 12345, - client: { redirectUri: 'http://example.com/cb' } + client: client }); }) .catch(should.fail); @@ -346,73 +347,72 @@ describe('AuthorizeHandler', function() { }); }); - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { + describe('getClient()', function() { + it('should throw an error if `client_id` is missing', function() { var model = { getAccessToken: function() {}, getClient: function() {}, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request) + return handler.getClient(request) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `scope`'); + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `client_id`'); }); }); - it('should return the scope', function() { + it('should throw an error if `client_id` is invalid', function() { var model = { getAccessToken: function() {}, getClient: function() {}, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); + var request = new Request({ body: { client_id: 'ø倣‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request) - .then(function(scope) { - scope.should.equal('foo'); - }) - .catch(should.fail); + return handler.getClient(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + }); }); - }); - describe('getClient()', function() { - it('should throw an error if `client_id` is missing', function() { + it('should throw an error if `client.redirectUri` is invalid', function() { var model = { getAccessToken: function() {}, getClient: function() {}, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); + var request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `client_id`'); + e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); }); }); - it('should throw an error if `client_id` is invalid', function() { + it('should throw an error if `client` is missing', function() { var model = { getAccessToken: function() {}, getClient: function() {}, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 'ø倣‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); + 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(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client credentials are invalid'); }); }); @@ -433,10 +433,48 @@ describe('AuthorizeHandler', function() { }); }); + it('should throw an error if `client.grants` is missing', function() { + var model = { + getAccessToken: function() {}, + getClient: function() { + return {}; + }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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: [] }; + }, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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 {}; }, + getClient: function() { return { grants: ['authorization_code'] }; }, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); @@ -450,20 +488,22 @@ describe('AuthorizeHandler', function() { }); }); - it('should throw an error if `client.redirectUri` is invalid', function() { + it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', function() { var model = { getAccessToken: function() {}, - getClient: function() { return { redirectUri: 'foobar' }; }, + getClient: function() { + return { grants: ['authorization_code'], redirectUri: 'https://example.com' }; + }, saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); + 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: `redirectUri` is not a valid URI'); + e.message.should.equal('Invalid client: `redirect_uri` does not match client value'); }); }); @@ -471,7 +511,7 @@ describe('AuthorizeHandler', function() { var model = { getAccessToken: function() {}, getClient: function() { - return Promise.resolve({ redirectUri: 'http://example.com/cb' }); + return Promise.resolve({ grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }); }, saveAuthCode: function() {} }; @@ -490,7 +530,7 @@ describe('AuthorizeHandler', function() { var model = { getAccessToken: function() {}, getClient: function() { - return { redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, saveAuthCode: function() {} }; @@ -507,7 +547,7 @@ describe('AuthorizeHandler', function() { describe('with `client_id` in the request body', function() { it('should return a client', function() { - var client = { redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; var model = { getAccessToken: function() {}, getClient: function() { @@ -528,7 +568,7 @@ describe('AuthorizeHandler', function() { describe('with `client_id` in the request query', function() { it('should return a client', function() { - var client = { redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; var model = { getAccessToken: function() {}, getClient: function() { @@ -548,6 +588,41 @@ describe('AuthorizeHandler', function() { }); }); + describe('getScope()', function() { + it('should throw an error if `scope` is invalid', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + }); + }); + + it('should return the scope', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request) + .then(function(scope) { + scope.should.equal('foo'); + }) + .catch(should.fail); + }); + }); + describe('getState()', function() { it('should throw an error if `state` is missing', function() { var model = { @@ -687,49 +762,6 @@ describe('AuthorizeHandler', function() { }); }); - describe('buildSuccessRedirectUri()', function() { - it('should return a redirect uri', function() { - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthCode: function() {} - }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var responseType = new CodeResponseType(12345); - var redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); - - url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); - }); - }); - - describe('buildErrorRedirectUri()', function() { - it('should set `error_description` if available', function() { - var error = new InvalidClientError('foo bar'); - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthCode: function() {} - }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error) - - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); - }); - - it('should return a redirect uri', function() { - var error = new InvalidClientError(); - var model = { - getAccessToken: function() {}, - getClient: function() {}, - saveAuthCode: function() {} - }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); - var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client'); - }); - }); - describe('getResponseType()', function() { it('should throw an error if `response_type` is missing', function() { var model = { @@ -799,4 +831,64 @@ describe('AuthorizeHandler', function() { }); }); }); + + describe('buildSuccessRedirectUri()', function() { + it('should return a redirect uri', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var responseType = new CodeResponseType(12345); + var redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); + + url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); + }); + }); + + describe('buildErrorRedirectUri()', function() { + it('should set `error_description` if available', function() { + var error = new InvalidClientError('foo bar'); + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); + + url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); + }); + + it('should return a redirect uri', function() { + var error = new InvalidClientError(); + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); + + url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client'); + }); + }); + + describe('updateResponse()', function() { + it('should set the `location` header', function() { + var model = { + getAccessToken: function() {}, + getClient: function() {}, + saveAuthCode: function() {} + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var response = new Response({ body: {}, headers: {} }); + var uri = url.parse('http://example.com/cb'); + + handler.updateResponse(response, uri, 'foobar'); + + response.get('location').should.equal('http://example.com/cb?state=foobar'); + }); + }); }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 7960146c7..7e5761234 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -14,6 +14,7 @@ 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'); @@ -217,7 +218,7 @@ describe('TokenHandler', function() { it('should throw a server error if a non-oauth error is thrown', function() { var model = { getClient: function() { - return {}; + return { grants: ['password'] }; }, getUser: function() { return {}; @@ -252,7 +253,7 @@ describe('TokenHandler', function() { it('should update the response if an error is thrown', function() { var model = { getClient: function() { - return {}; + return { grants: ['password'] }; }, getUser: function() { return {}; @@ -288,7 +289,7 @@ describe('TokenHandler', function() { var token = { accessToken: 'foo', refreshToken: 'bar', accessTokenLifetime: 120, scope: 'foobar' }; var model = { getClient: function() { - return {}; + return { grants: ['password'] }; }, getUser: function() { return {}; @@ -363,38 +364,46 @@ describe('TokenHandler', function() { }); describe('generateRefreshToken()', function() { - it('should return a refresh token', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + describe('if the client does not support the `refresh_token` grant', function() { + it('should not return a refresh token', function *() { + var client = { + grants: [] + }; + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.generateRefreshToken() - .then(function(data) { - data.should.be.a.sha1; - }) - .catch(should.fail); + return handler.generateRefreshToken(client) + .then(function(data) { + should.not.exist(data); + }) + .catch(should.fail); + }); }); - }); - describe('getAccessTokenLifetime()', function() { - it('should return a date', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + describe('if the client supports the `refresh_token` grant', function() { + it('should return a refresh token', function() { + var client = { + grants: ['refresh_token'] + }; + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.getAccessTokenLifetime() - .then(function(data) { - data.should.be.an.instanceOf(Date); - }) - .catch(should.fail); + return handler.generateRefreshToken(client) + .then(function(data) { + data.should.be.a.sha1; + }) + .catch(should.fail); + }); }); }); - describe('getRefreshTokenLifetime()', function() { + describe('getAccessTokenExpiresOn()', function() { it('should return a date', function() { var model = { getClient: function() {}, @@ -402,7 +411,7 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.getRefreshTokenLifetime() + return handler.getAccessTokenExpiresOn() .then(function(data) { data.should.be.an.instanceOf(Date); }) @@ -410,75 +419,60 @@ describe('TokenHandler', function() { }); }); - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { + 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: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + var request = new Request({ body: { client_id: 'ø倣‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request) + return handler.getClient(request) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `scope`'); + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); }); }); - it('should return the scope', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - return handler.getScope(request) - .then(function(scope) { - scope.should.equal('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: {} }); + var request = new Request({ body: { client_id: 'foo', client_secret: 'ø倣‰' }, headers: {}, method: {}, query: {} }); return handler.getClient(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); + e.message.should.equal('Invalid parameter: `client_secret`'); }); }); - it('should throw an error if `clientId` is invalid', function() { + 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: 'foo', client_secret: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + 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(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_secret`'); + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); }); }); - it('should throw an error if `client` is missing', function() { + it('should throw an error if `client.grants` is missing', function() { var model = { - getClient: function() {}, + getClient: function() { + return {}; + }, saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -488,12 +482,12 @@ describe('TokenHandler', function() { .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client is invalid'); + e.message.should.equal('Invalid client: missing client `grants`'); }); }); it('should return a client', function() { - var client = { id: 12345 }; + var client = { id: 12345, grants: [] }; var model = { getClient: function() { return client; @@ -513,7 +507,7 @@ describe('TokenHandler', function() { it('should support promises', function() { var model = { getClient: function() { - return Promise.resolve({}); + return Promise.resolve({ grants: [] }); }, saveToken: function() {} }; @@ -526,7 +520,7 @@ describe('TokenHandler', function() { it('should support non-promises', function() { var model = { getClient: function() { - return {}; + return { grants: [] }; }, saveToken: function() {} }; @@ -537,6 +531,79 @@ describe('TokenHandler', function() { }); }); + describe('getRefreshTokenExpiresOn()', function() { + describe('if the client does not support the `refresh_token` grant', function() { + it('should not return a refresh token', function *() { + var client = { + grants: [''] + }; + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.getRefreshTokenExpiresOn(client) + .then(function(data) { + should.not.exist(data); + }) + .catch(should.fail); + }); + }); + + describe('if the client supports the `refresh_token` grant', function() { + it('should return a refresh token', function() { + var client = { + grants: ['refresh_token'] + }; + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.getRefreshTokenExpiresOn(client) + .then(function(data) { + data.should.be.an.instanceOf(Date); + }) + .catch(should.fail); + }); + }); + }); + + describe('getScope()', function() { + it('should throw an error if `scope` is invalid', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + }); + }); + + it('should return the scope', function() { + var model = { + getClient: function() {}, + saveToken: function() {} + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); + + return handler.getScope(request) + .then(function(scope) { + scope.should.equal('foo'); + }) + .catch(should.fail); + }); + }); + describe('getClientCredentials()', function() { it('should throw an error if `client_id` is missing', function() { var model = { @@ -659,7 +726,25 @@ describe('TokenHandler', function() { }); }); + 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: {} }); + + return handler.handleGrantType(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal('Unauthorized client: `grant_type` is invalid'); + }); + }); + it('should return a grant type result if the `grant_type` is a uri', function() { + var client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; var user = {}; var model = { getClient: function() {}, @@ -671,7 +756,7 @@ describe('TokenHandler', function() { 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) + return handler.handleGrantType(request, client) .then(function(data) { data.should.equal(user); }) @@ -679,6 +764,7 @@ describe('TokenHandler', function() { }); it('should return a grant type result if the `grant_type` is not a uri', function() { + var client = { grants: ['password'] }; var user = {}; var model = { getClient: function() {}, @@ -690,7 +776,7 @@ describe('TokenHandler', 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) + return handler.handleGrantType(request, client) .then(function(data) { data.should.equal(user); }) @@ -699,6 +785,19 @@ describe('TokenHandler', function() { }); describe('saveToken()', function() { + it('should set `refreshToken` if `refreshToken` is defined', function() { + var model = { + getClient: function() {}, + saveToken: function(token) { + token.should.have.properties('refreshToken', 'refreshTokenExpiresOn'); + } + }; + var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); + + return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz') + .catch(should.fail); + }); + it('should return a token', function() { var token = {}; var model = { @@ -745,7 +844,7 @@ describe('TokenHandler', function() { describe('with grant_type `authorization_code`', function() { it('should return a user', function() { var authCode = { client: { id: 'foobar' }, user: {} }; - var client = { id: 'foobar' }; + var client = { id: 'foobar', grants: ['authorization_code'] }; var model = { getAuthCode: function() { return authCode; @@ -774,6 +873,7 @@ describe('TokenHandler', function() { describe('with grant_type `client_credentials`', function() { it('should return a user', function() { + var client = { grants: ['client_credentials'] }; var user = {}; var model = { getClient: function() {}, @@ -792,7 +892,7 @@ describe('TokenHandler', function() { query: {} }); - return handler.handleGrantType(request, {}) + return handler.handleGrantType(request, client) .then(function(data) { data.should.equal(user); }) @@ -802,6 +902,7 @@ describe('TokenHandler', function() { describe('with grant_type `password`', function() { it('should return a user', function() { + var client = { grants: ['password'] }; var user = {}; var model = { getClient: function() {}, @@ -824,7 +925,7 @@ describe('TokenHandler', function() { query: {} }); - return handler.handleGrantType(request) + return handler.handleGrantType(request, client) .then(function(data) { data.should.equal(user); }) @@ -834,7 +935,7 @@ describe('TokenHandler', function() { describe('with grant_type `refresh_token`', function() { it('should return a user', function() { - var client = {}; + var client = { grants: ['refresh_token'] }; var refreshToken = { client: {}, user: {} }; var model = { getClient: function() {}, @@ -944,7 +1045,7 @@ describe('TokenHandler', function() { saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); + var tokenType = handler.getTokenType('foo', 'bar', 'foobar'); tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: 120, refreshToken: 'bar', scope: 'foobar' }); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index a3fb16814..3d2f577be 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -87,7 +87,7 @@ describe('Server', function() { return { user: {} }; }, getClient: function() { - return { redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, saveAuthCode: function() { return { authCode: 123 }; @@ -107,7 +107,7 @@ describe('Server', function() { return { user: {} }; }, getClient: function() { - return { redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, saveAuthCode: function() { return { authCode: 123 }; @@ -125,7 +125,7 @@ describe('Server', function() { it('should return a promise', function() { var model = { getClient: function() { - return {}; + return { grants: ['password'] }; }, getUser: function() { return {}; @@ -145,7 +145,7 @@ describe('Server', function() { it('should support callbacks', function(next) { var model = { getClient: function() { - return {}; + return { grants: ['password'] }; }, getUser: function() { return {}; diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 307c5bd74..7bea2d8da 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -36,7 +36,7 @@ describe('AuthorizeHandler', function() { it('should call `model.getClient()`', function() { var model = { getAccessToken: function() {}, - getClient: sinon.stub().returns({ redirectUri: 'http://example.com/cb' }), + getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }), saveAuthCode: function() {} }; var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index 373ccd910..3810dbea5 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -33,6 +33,9 @@ describe('TokenHandler', function() { describe('generateRefreshToken()', function() { it('should call `model.generateRefreshToken()`', function() { + var client = { + grants: ['refresh_token'] + }; var model = { generateRefreshToken: sinon.spy(), getClient: function() {}, @@ -40,7 +43,7 @@ describe('TokenHandler', function() { }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - return handler.generateRefreshToken() + return handler.generateRefreshToken(client) .then(function() { model.generateRefreshToken.callCount.should.equal(1); model.generateRefreshToken.firstCall.args.should.have.length(0); @@ -52,7 +55,7 @@ describe('TokenHandler', function() { describe('getClient()', function() { it('should call `model.getClient()`', function() { var model = { - getClient: sinon.stub().returns({}), + getClient: sinon.stub().returns({ grants: ['password'] }), saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); From d0987fa68569e34edf07e1ed21c98dbc6761d2a8 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 6 Apr 2015 17:43:06 +0100 Subject: [PATCH 11/39] Improve error handling --- lib/errors/access-denied-error.js | 10 ++-- lib/errors/invalid-argument-error.js | 11 +++-- lib/errors/invalid-client-error.js | 10 ++-- lib/errors/invalid-grant-error.js | 10 ++-- lib/errors/invalid-request-error.js | 10 ++-- lib/errors/invalid-scope-error.js | 11 +++-- lib/errors/invalid-token-error.js | 10 ++-- lib/errors/oauth-error.js | 9 ++-- lib/errors/server-error.js | 10 ++-- lib/errors/unauthorized-client-error.js | 10 ++-- lib/errors/unsupported-grant-type-error.js | 12 +++-- lib/handlers/authorize-handler.js | 2 +- lib/handlers/token-handler.js | 16 +++++-- package.json | 2 +- .../authorization-code-grant-type_test.js | 4 +- .../client-credentials-grant-type_test.js | 4 +- .../grant-types/password-grant-type_test.js | 4 +- .../refresh-token-grant-type_test.js | 4 +- .../handlers/authenticate-handler_test.js | 4 +- .../handlers/authorize-handler_test.js | 41 +++++++++++++++-- .../handlers/token-handler_test.js | 46 +++++++++++++++++-- test/integration/request_test.js | 4 +- .../response-types/code-response-type_test.js | 4 +- test/integration/response_test.js | 4 +- test/integration/server_test.js | 4 +- .../token-types/bearer-token-type_test.js | 4 +- test/integration/utils/token-util_test.js | 4 +- 27 files changed, 185 insertions(+), 79 deletions(-) diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js index e2f6a3dc3..4173f14eb 100644 --- a/lib/errors/access-denied-error.js +++ b/lib/errors/access-denied-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function AccessDeniedError(message) { - OAuthError.call(this, { +function AccessDeniedError(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'access_denied' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index 234ca83ad..b393f8f8c 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -2,6 +2,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('standard-error'); var util = require('util'); @@ -9,11 +10,13 @@ var util = require('util'); * Constructor. */ -function InvalidArgumentError(message) { - OAuthError.call(this, { +function InvalidArgumentError(message, properties) { + properties = _.assign({ code: 500, - message: message - }); + name: 'invalid_argument' + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js index 189c17fff..83f04dd0f 100644 --- a/lib/errors/invalid-client-error.js +++ b/lib/errors/invalid-client-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function InvalidClientError(message) { - OAuthError.call(this, { +function InvalidClientError(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'invalid_client' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js index 3b31529ca..4fe105b1d 100644 --- a/lib/errors/invalid-grant-error.js +++ b/lib/errors/invalid-grant-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function InvalidGrantError(message) { - OAuthError.call(this, { +function InvalidGrantError(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'invalid_grant' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js index a0417465f..4ccc960a4 100644 --- a/lib/errors/invalid-request-error.js +++ b/lib/errors/invalid-request-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function InvalidRequest(message) { - OAuthError.call(this, { +function InvalidRequest(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'invalid_request' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index 57aa826ca..1a79439af 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,14 @@ var util = require('util'); * Constructor. */ -function InvalidScopeError(message) { - OAuthError.call(this, { +function InvalidScopeError(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'invalid_scope' - }); + }, properties); + + OAuthError.call(this, message, properties); + } /** diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js index e6d435a63..24a2d548d 100644 --- a/lib/errors/invalid-token-error.js +++ b/lib/errors/invalid-token-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function InvalidTokenError(message) { - OAuthError.call(this, { +function InvalidTokenError(message, properties) { + properties = _.assign({ code: 401, - message: message, name: 'invalid_token' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js index 0a5178221..ba2c11bac 100644 --- a/lib/errors/oauth-error.js +++ b/lib/errors/oauth-error.js @@ -3,15 +3,18 @@ * Module dependencies. */ -var StandardError = require("standard-error"); +var StandardError = require('standard-error'); var util = require('util'); /** * Constructor. */ -function OAuthError(message, properties) { - StandardError.call(this, message, properties); +function OAuthError(messageOrError, properties) { + var message = messageOrError instanceof Error ? messageOrError.message : messageOrError; + var error = messageOrError instanceof Error ? messageOrError : null; + + StandardError.call(this, message, properties, error); } /** diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js index 43fc25104..ec5ac0860 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function ServerError(message) { - OAuthError.call(this, { +function ServerError(message, properties) { + properties = _.assign({ code: 503, - message: message, name: 'server_error' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js index e56e9e445..7026e872a 100644 --- a/lib/errors/unauthorized-client-error.js +++ b/lib/errors/unauthorized-client-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,12 +11,13 @@ var util = require('util'); * Constructor. */ -function UnauthorizedClientError(message) { - OAuthError.call(this, { +function UnauthorizedClientError(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'unauthorized_client' - }); + }, properties); + + OAuthError.call(this, message, properties); } /** diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index 402025fea..ad17877f8 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -3,6 +3,7 @@ * Module dependencies. */ +var _ = require('lodash'); var OAuthError = require('./oauth-error'); var util = require('util'); @@ -10,13 +11,14 @@ var util = require('util'); * Constructor. */ -function UnsupportedGrantTypeError(message) { - OAuthError.call(this, { +function UnsupportedGrantTypeError(message, properties) { + properties = _.assign({ code: 400, - message: message, name: 'unsupported_grant_type' - }); -} + }, properties); + + OAuthError.call(this, message, properties); + } /** * Inherit prototype. diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 4f4b3271e..d694ca31c 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -97,7 +97,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { }) .catch(function(e) { if (!(e instanceof OAuthError)) { - e = new ServerError(e.message); + e = new ServerError(e); } var redirectUri = this.buildErrorRedirectUri(client.redirectUri, e); diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index accfb94b4..62914d552 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -7,6 +7,7 @@ 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 InvalidGrantError = require('../errors/invalid-grant-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); @@ -117,7 +118,7 @@ TokenHandler.prototype.handle = function(request, response) { }); }).catch(function(e) { if (!(e instanceof OAuthError)) { - e = new ServerError(e.message); + e = new ServerError(e); } this.updateErrorResponse(response, e); @@ -196,11 +197,11 @@ TokenHandler.prototype.getClient = Promise.method(function(request) { } if (!client.grants) { - throw new InvalidClientError('Invalid client: missing client `grants`'); + throw new ServerError('Server error: missing client `grants`'); } if (!(client.grants instanceof Array)) { - throw new InvalidClientError('Invalid client: `grants` must be an array'); + throw new ServerError('Server error: `grants` must be an array'); } return client; @@ -284,7 +285,14 @@ TokenHandler.prototype.handleGrantType = Promise.method(function(request, client var Type = this.grantTypes[grantType]; return new Type(this.model) - .handle(request, client); + .handle(request, client) + .catch(function(e) { + if (!(e instanceof OAuthError)) { + throw new InvalidGrantError('Invalid grant: credentials are invalid'); + } + + throw e; + }); }); /** diff --git a/package.json b/package.json index d444fdb8a..c06cd337e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "bluebird": "^2.9.13", "camel-case": "^1.1.1", "lodash": "^3.3.1", - "standard-error": "^1.1.0", + "standard-error": "git+ssh://git@github.com/seegno-forks/js-standard-error#feature/add-nested-error-support", "type-is": "^1.6.0", "validator.js": "git+ssh://git@github.com/seegno-forks/validator.js#enhancement/add-extend-method-to-assert" }, diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index c5a399343..a98817dc9 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -14,10 +14,10 @@ var sinon = require('sinon'); var should = require('should'); /** - * Test `AuthorizationCodeGrantType`. + * Test `AuthorizationCodeGrantType` integration. */ -describe('AuthorizationCodeGrantType', function() { +describe('AuthorizationCodeGrantType integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { try { diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 518f7a257..cf483bf95 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -13,10 +13,10 @@ var sinon = require('sinon'); var should = require('should'); /** - * Test `ClientCredentialsGrantType`. + * Test `ClientCredentialsGrantType` integration. */ -describe('ClientCredentialsGrantType', function() { +describe('ClientCredentialsGrantType integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { try { diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index af7d377da..bc3a8ed82 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -14,10 +14,10 @@ var sinon = require('sinon'); var should = require('should'); /** - * Test `PasswordGrantType`. + * Test `PasswordGrantType` integration. */ -describe('PasswordGrantType', function() { +describe('PasswordGrantType integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { try { diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 2e6eb3479..7e7834611 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -14,10 +14,10 @@ var sinon = require('sinon'); var should = require('should'); /** - * Test `RefreshTokenGrantType`. + * Test `RefreshTokenGrantType` integration. */ -describe('RefreshTokenGrantType', function() { +describe('RefreshTokenGrantType integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { try { diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 758a328df..b5164f9db 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -14,10 +14,10 @@ var ServerError = require('../../../lib/errors/server-error'); var should = require('should'); /** - * Test `AuthenticateHandler`. + * Test `AuthenticateHandler` integration. */ -describe('AuthenticateHandler', function() { +describe('AuthenticateHandler integration', function() { describe('constructor()', function() { it('should throw an error if `options.model` is missing', function() { try { diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 7217b1679..d3ef84ade 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -19,10 +19,10 @@ var should = require('should'); var url = require('url'); /** - * Test `AuthorizeHandler`. + * Test `AuthorizeHandler` integration. */ -describe('AuthorizeHandler', function() { +describe('AuthorizeHandler integration', function() { describe('constructor()', function() { it('should throw an error if `options.authCodeLifetime` is missing', function() { try { @@ -174,7 +174,42 @@ describe('AuthorizeHandler', function() { }); }); - it('should redirect to an error response if an error is thrown', function() { + it('should redirect to an error response if a non-oauth error is thrown', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + }, + getClient: function() { + return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + }, + saveAuthCode: function() { + throw new Error('Unhandled exception'); + } + }; + var handler = new AuthorizeHandler({ authCodeLifetime: 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: {} }; diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 7e5761234..b0dbb01d0 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -7,6 +7,7 @@ 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'); @@ -20,10 +21,10 @@ var should = require('should'); var util = require('util'); /** - * Test `TokenHandler`. + * Test `TokenHandler` integration. */ -describe('TokenHandler', function() { +describe('TokenHandler integration', function() { describe('constructor()', function() { it('should throw an error if `options.accessTokenLifetime` is missing', function() { try { @@ -247,6 +248,7 @@ describe('TokenHandler', function() { .catch(function(e) { e.should.be.an.instanceOf(ServerError); e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); }); }); @@ -481,8 +483,26 @@ describe('TokenHandler', function() { 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`'); + 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'); }); }); @@ -743,6 +763,24 @@ describe('TokenHandler', function() { }); }); + it('should throw an invalid grant error if a non-oauth error is thrown', function() { + var client = { grants: ['password'] }; + var model = { + getClient: function() {}, + getUser: function() {}, + 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'); + }); + }); + it('should return a grant type result if the `grant_type` is a uri', function() { var client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; var user = {}; diff --git a/test/integration/request_test.js b/test/integration/request_test.js index 0e9743f11..63f6cd919 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -8,10 +8,10 @@ var InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); var should = require('should'); /** - * Test `Request`. + * Test `Request` integration. */ -describe('Request', function() { +describe('Request integration', function() { describe('constructor()', function() { it('should throw an error if `headers` is missing', function() { try { diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index 524703363..844b9c8ed 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -9,10 +9,10 @@ var should = require('should'); var url = require('url'); /** - * Test `CodeResponseType`. + * Test `CodeResponseType` integration. */ -describe('CodeResponseType', function() { +describe('CodeResponseType integration', function() { describe('constructor()', function() { it('should throw an error if `code` is missing', function() { try { diff --git a/test/integration/response_test.js b/test/integration/response_test.js index f1cc7b10c..774c6cfc4 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -6,10 +6,10 @@ var Response = require('../../lib/response'); /** - * Test `Response`. + * Test `Response` integration. */ -describe('Response', function() { +describe('Response integration', function() { describe('constructor()', function() { it('should set the `body`', function() { var response = new Response({ body: 'foo', headers: {} }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 3d2f577be..9a4218544 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -11,10 +11,10 @@ var Server = require('../../lib/server'); var should = require('should'); /** - * Test `Server`. + * Test `Server` integration. */ -describe('Server', function() { +describe('Server integration', function() { describe('constructor()', function() { it('should throw an error if `model` is missing', function() { try { diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js index b52e705ea..66943ede5 100644 --- a/test/integration/token-types/bearer-token-type_test.js +++ b/test/integration/token-types/bearer-token-type_test.js @@ -8,10 +8,10 @@ var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error') var should = require('should'); /** - * Test `BearerTokenType`. + * Test `BearerTokenType` integration. */ -describe('BearerTokenType', function() { +describe('BearerTokenType integration', function() { describe('constructor()', function() { it('should throw an error if `accessToken` is missing', function() { try { diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js index e2ac9fc91..d35fd3f79 100644 --- a/test/integration/utils/token-util_test.js +++ b/test/integration/utils/token-util_test.js @@ -7,10 +7,10 @@ var TokenUtil = require('../../../lib/utils/token-util'); var should = require('should'); /** - * Test `TokenUtil`. + * Test `TokenUtil` integration. */ -describe('TokenUtil', function() { +describe('TokenUtil integration', function() { describe('generateRandomToken()', function() { it('should return a sha-1 token', function() { return TokenUtil.generateRandomToken() From cebca4edf45805a6700b9e1ae5bcae48bb79679b Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 6 Apr 2015 18:59:58 +0100 Subject: [PATCH 12/39] Add WWW-Authenticate header See: https://tools.ietf.org/html/rfc6749#section-5.2 --- lib/handlers/token-handler.js | 17 +++++++++++-- .../handlers/token-handler_test.js | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 62914d552..52064b502 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -87,7 +87,7 @@ TokenHandler.prototype.handle = function(request, response) { return Promise.bind(this) .then(function() { - return this.getClient(request); + return this.getClient(request, response); }) .then(function(client) { return this.handleGrantType(request, client) @@ -171,7 +171,7 @@ TokenHandler.prototype.getAccessTokenExpiresOn = Promise.method(function() { * Get the client from the model. */ -TokenHandler.prototype.getClient = Promise.method(function(request) { +TokenHandler.prototype.getClient = Promise.method(function(request, response) { var credentials = this.getClientCredentials(request); if (!credentials.clientId) { @@ -205,6 +205,19 @@ TokenHandler.prototype.getClient = Promise.method(function(request) { } 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; }); }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index b0dbb01d0..48f8309c7 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -506,6 +506,31 @@ describe('TokenHandler integration', function() { }); }); + 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 = { From aa77f9086e0330be58c26280830953596fb7a0ce Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Fri, 10 Apr 2015 11:50:07 +0100 Subject: [PATCH 13/39] Improve authorization code expiration handling --- lib/grant-types/authorization-code-grant-type.js | 8 ++++---- lib/grant-types/refresh-token-grant-type.js | 2 +- .../grant-types/authorization-code-grant-type_test.js | 6 +++--- test/integration/handlers/token-handler_test.js | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 2295a24af..d0809b0b8 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -27,7 +27,7 @@ function AuthCodeGrantType(model) { } /** - * Retrieve the user from the model using an authorization code. + * Retrieve an authorization code from the model. * * (See: https://tools.ietf.org/html/rfc6749#section-4.1.3) */ @@ -67,11 +67,11 @@ AuthCodeGrantType.prototype.handle = function(request, client) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (authCode.expiresOn && !(authCode.expiresOn instanceof Date)) { - throw new ServerError('Server error: `expires` must be a Date instance'); + if (!(authCode.expiresOn instanceof Date)) { + throw new ServerError('Server error: `expiresOn` must be a Date instance'); } - if (authCode.expiresOn && authCode.expiresOn < new Date()) { + if (authCode.expiresOn < new Date()) { throw new InvalidGrantError('Invalid grant: authorization code has expired'); } diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 909b41f2b..fe65019e6 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -27,7 +27,7 @@ function RefreshTokenGrantType(model) { } /** - * Retrieve the user from the model using a refresh_token. + * Retrieve a refresh token from the model. * * (See: https://tools.ietf.org/html/rfc6749#section-6) */ diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index a98817dc9..353a021e4 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -201,7 +201,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should return an auth code', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, user: {} }; + var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { getAuthCode: sinon.stub().returns(authCode) @@ -217,7 +217,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should support promises', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, user: {} }; + var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { getAuthCode: function() { @@ -231,7 +231,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should support non-promises', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, user: {} }; + var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { getAuthCode: function() { diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 48f8309c7..27e8507cb 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -906,7 +906,7 @@ describe('TokenHandler integration', function() { describe('handleGrantType()', function() { describe('with grant_type `authorization_code`', function() { it('should return a user', function() { - var authCode = { client: { id: 'foobar' }, user: {} }; + var authCode = { client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar', grants: ['authorization_code'] }; var model = { getAuthCode: function() { From 779afe74d684eda6f7cc68b545834ac3231e6ff2 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 13 Apr 2015 17:29:14 +0100 Subject: [PATCH 14/39] Improve token grant architecture --- lib/grant-types/abstract-grant-type.js | 95 ++++ .../authorization-code-grant-type.js | 96 +++- .../client-credentials-grant-type.js | 76 ++- lib/grant-types/password-grant-type.js | 76 ++- lib/grant-types/refresh-token-grant-type.js | 119 ++++- lib/handlers/authenticate-handler.js | 34 +- lib/handlers/authorize-handler.js | 4 +- lib/handlers/token-handler.js | 161 +----- lib/models/token-model.js | 48 ++ .../grant-types/abstract-grant-type_test.js | 173 +++++++ .../authorization-code-grant-type_test.js | 300 +++++++++-- .../client-credentials-grant-type_test.js | 155 ++++-- .../grant-types/password-grant-type_test.js | 201 ++++++-- .../refresh-token-grant-type_test.js | 381 +++++++++++--- .../handlers/authenticate-handler_test.js | 43 +- .../handlers/authorize-handler_test.js | 12 +- .../handlers/token-handler_test.js | 468 ++---------------- test/integration/server_test.js | 4 +- .../grant-types/abstract-grant-type_test.js | 44 ++ .../authorization-code-grant-type_test.js | 82 +++ .../client-credentials-grant-type_test.js | 60 +++ .../grant-types/password-grant-type_test.js | 62 +++ .../refresh-token-grant-type_test.js | 106 ++++ test/unit/handlers/token-handler_test.js | 59 --- 24 files changed, 1983 insertions(+), 876 deletions(-) create mode 100644 lib/grant-types/abstract-grant-type.js create mode 100644 lib/models/token-model.js create mode 100644 test/integration/grant-types/abstract-grant-type_test.js create mode 100644 test/unit/grant-types/abstract-grant-type_test.js create mode 100644 test/unit/grant-types/authorization-code-grant-type_test.js create mode 100644 test/unit/grant-types/client-credentials-grant-type_test.js create mode 100644 test/unit/grant-types/password-grant-type_test.js create mode 100644 test/unit/grant-types/refresh-token-grant-type_test.js diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js new file mode 100644 index 000000000..5f85aba59 --- /dev/null +++ b/lib/grant-types/abstract-grant-type.js @@ -0,0 +1,95 @@ + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var Promise = require('bluebird'); +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; +} + +/** + * Generate access token. + */ + +AbstractGrantType.prototype.generateAccessToken = Promise.method(function() { + if (this.model.generateAccessToken) { + return this.model.generateAccessToken(); + } + + return tokenUtil.generateRandomToken(); +}); + +/** + * Generate refresh token. + */ + +AbstractGrantType.prototype.generateRefreshToken = Promise.method(function() { + if (this.model.generateRefreshToken) { + return this.model.generateRefreshToken(); + } + + return tokenUtil.generateRandomToken(); +}); + +/** + * Get access token expires on. + */ + +AbstractGrantType.prototype.getAccessTokenExpiresOn = function() { + var expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); + + return expires; +}; + +/** + * Get refresh token expires on. + */ + +AbstractGrantType.prototype.getRefreshTokenExpiresOn = function() { + var expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); + + return expires; +}; + +/** + * Get scope from the request body. + */ + +AbstractGrantType.prototype.getScope = function(request) { + if (!is.nqschar(request.body.scope)) { + throw new InvalidArgumentError('Invalid parameter: `scope`'); + } + + return request.body.scope; +}; + +/** + * Export constructor. + */ + +module.exports = AbstractGrantType; diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index d0809b0b8..76afc3071 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -3,33 +3,51 @@ * 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 ServerError = require('../errors/server-error'); var is = require('../validator/is'); +var util = require('util'); /** * Constructor. */ -function AuthCodeGrantType(model) { - if (!model) { +function AuthCodeGrantType(options) { + options = options || {}; + + if (!options.model) { throw new InvalidArgumentError('Missing parameter: `model`'); } - if (!model.getAuthCode) { - throw new ServerError('Server error: model does not implement `getAuthCode()`'); + if (!options.model.getAuthCode) { + throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthCode()`'); + } + + if (!options.model.revokeAuthCode) { + throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthCode()`'); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); } - this.model = model; + AbstractGrantType.call(this, options); } /** - * Retrieve an authorization code from the model. + * Inherit prototype. + */ + +util.inherits(AuthCodeGrantType, AbstractGrantType); + +/** + * Handle authorization code grant. * - * (See: https://tools.ietf.org/html/rfc6749#section-4.1.3) + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ AuthCodeGrantType.prototype.handle = function(request, client) { @@ -41,6 +59,23 @@ AuthCodeGrantType.prototype.handle = function(request, client) { throw new InvalidArgumentError('Missing parameter: `client`'); } + return Promise.bind(this) + .then(function() { + return this.getAuthCode(request, client); + }) + .tap(function(code) { + return this.revokeAuthCode(code); + }) + .then(function(code) { + return this.saveToken(code.user, client, code.authCode, code.scope); + }); +}; + +/** + * Get the authorization code. + */ + +AuthCodeGrantType.prototype.getAuthCode = function(request, client) { if (!request.body.code) { return Promise.reject(new InvalidRequestError('Missing parameter: `code`')); } @@ -79,6 +114,53 @@ AuthCodeGrantType.prototype.handle = function(request, client) { }); }; +/** + * 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 + */ + +AuthCodeGrantType.prototype.revokeAuthCode = Promise.method(function(authCode) { + return Promise.try(this.model.revokeAuthCode, authCode) + .then(function(authCode) { + if (!authCode) { + throw new InvalidGrantError('Invalid grant: authorization code is invalid'); + } + + if (!(authCode.expiresOn instanceof Date)) { + throw new ServerError('Server error: `expiresOn` must be a Date instance'); + } + + if (authCode.expiresOn >= new Date()) { + throw new ServerError('Server error: authorization code should be expired'); + } + + return authCode; + }); +}); + +/** + * Save token. + */ + +AuthCodeGrantType.prototype.saveToken = function(user, client, authCode, scope) { + return this.generateAccessToken() + .bind(this) + .then(function(accessToken) { + var token = { + accessToken: accessToken, + authCode: authCode, + scope: scope + }; + + return this.model.saveToken(token, client, user); + }); +}; + /** * Export constructor. */ diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index 0bad54a55..b12de67d4 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -3,34 +3,47 @@ * 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 ServerError = require('../errors/server-error'); +var util = require('util'); /** * Constructor. */ -function ClientCredentialsType(model) { - if (!model) { +function ClientCredentialsGrantType(options) { + options = options || {}; + + if (!options.model) { throw new InvalidArgumentError('Missing parameter: `model`'); } - if (!model.getUserFromClient) { - throw new ServerError('Server error: model does not implement `getUserFromClient()`'); + 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()`'); } - this.model = model; + AbstractGrantType.call(this, options); } /** - * Retrieve the user from the model using client credentials. + * Inherit prototype. + */ + +util.inherits(ClientCredentialsGrantType, AbstractGrantType); + +/** + * Handle client credentials grant. * - * (See: https://tools.ietf.org/html/rfc6749#section-4.4.2) + * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 */ -ClientCredentialsType.prototype.handle = function(request, client) { +ClientCredentialsGrantType.prototype.handle = function(request, client) { if (!request) { throw new InvalidArgumentError('Missing parameter: `request`'); } @@ -39,6 +52,22 @@ ClientCredentialsType.prototype.handle = function(request, 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 Promise.try(this.model.getUserFromClient, client) .then(function(user) { if (!user) { @@ -49,8 +78,35 @@ ClientCredentialsType.prototype.handle = function(request, client) { }); }; +/** + * Save token. + */ + +ClientCredentialsGrantType.prototype.saveToken = function(user, client, scope) { + var fns = [ + this.generateAccessToken(), + this.generateRefreshToken(), + this.getAccessTokenExpiresOn(), + this.getRefreshTokenExpiresOn() + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn) { + var token = { + accessToken: accessToken, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshToken: refreshToken, + refreshTokenExpiresOn: refreshTokenExpiresOn, + scope: scope + }; + + return this.model.saveToken(token, client, user); + }); +}; + /** * Export constructor. */ -module.exports = ClientCredentialsType; +module.exports = ClientCredentialsGrantType; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index 4b3b5c49c..cbf6e5a23 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -3,40 +3,73 @@ * 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 ServerError = require('../errors/server-error'); var is = require('../validator/is'); +var util = require('util'); /** * Constructor. */ -function PasswordGrantType(model) { - if (!model) { +function PasswordGrantType(options) { + options = options || {}; + + if (!options.model) { throw new InvalidArgumentError('Missing parameter: `model`'); } - if (!model.getUser) { - throw new ServerError('Server error: model does not implement `getUser()`'); + 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()`'); } - this.model = model; + 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) + * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 */ -PasswordGrantType.prototype.handle = function(request) { +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) { return Promise.reject(new InvalidRequestError('Missing parameter: `username`')); } @@ -63,6 +96,33 @@ PasswordGrantType.prototype.handle = function(request) { }); }; +/** + * Save token. + */ + +PasswordGrantType.prototype.saveToken = function(user, client, scope) { + var fns = [ + this.generateAccessToken(), + this.generateRefreshToken(), + this.getAccessTokenExpiresOn(), + this.getRefreshTokenExpiresOn() + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn) { + var token = { + accessToken: accessToken, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshToken: refreshToken, + refreshTokenExpiresOn: refreshTokenExpiresOn, + scope: scope + }; + + return this.model.saveToken(token, client, user); + }); +}; + /** * Export constructor. */ diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index fe65019e6..383d17030 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -3,33 +3,51 @@ * 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 ServerError = require('../errors/server-error'); var is = require('../validator/is'); +var util = require('util'); /** * Constructor. */ -function RefreshTokenGrantType(model) { - if (!model) { +function RefreshTokenGrantType(options) { + options = options || {}; + + if (!options.model) { throw new InvalidArgumentError('Missing parameter: `model`'); } - if (!model.getRefreshToken) { - throw new ServerError('Server error: model does not implement `getRefreshToken()`'); + 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()`'); } - this.model = model; + AbstractGrantType.call(this, options); } /** - * Retrieve a refresh token from the model. + * Inherit prototype. + */ + +util.inherits(RefreshTokenGrantType, AbstractGrantType); + +/** + * Handle refresh token grant. * - * (See: https://tools.ietf.org/html/rfc6749#section-6) + * @see https://tools.ietf.org/html/rfc6749#section-6 */ RefreshTokenGrantType.prototype.handle = function(request, client) { @@ -41,6 +59,23 @@ RefreshTokenGrantType.prototype.handle = function(request, 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) { return Promise.reject(new InvalidRequestError('Missing parameter: `refresh_token`')); } @@ -50,32 +85,84 @@ RefreshTokenGrantType.prototype.handle = function(request, client) { } return Promise.try(this.model.getRefreshToken, request.body.refresh_token) - .then(function(refreshToken) { - if (!refreshToken) { + .then(function(token) { + if (!token) { throw new InvalidGrantError('Invalid grant: refresh token is invalid'); } - if (!refreshToken.client) { + if (!token.client) { throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object'); } - if (!refreshToken.user) { + if (!token.user) { throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object'); } - if (refreshToken.client.id !== client.id) { + if (token.client.id !== client.id) { throw new InvalidGrantError('Invalid grant: refresh token is invalid'); } - if (refreshToken.expires && !(refreshToken.expires instanceof Date)) { - throw new ServerError('Server error: `expires` must be a Date instance'); + if (token.refreshTokenExpiresOn && !(token.refreshTokenExpiresOn instanceof Date)) { + throw new ServerError('Server error: `refreshTokenExpiresOn` must be a Date instance'); } - if (refreshToken.expires && refreshToken.expires < new Date()) { + if (token.refreshTokenExpiresOn && token.refreshTokenExpiresOn < new Date()) { throw new InvalidGrantError('Invalid grant: refresh token has expired'); } - return refreshToken; + return token; + }); +}; + +/** + * Revoke the refresh token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + +RefreshTokenGrantType.prototype.revokeToken = Promise.method(function(token) { + return Promise.try(this.model.revokeToken, token) + .then(function(token) { + if (!token) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if (!(token.refreshTokenExpiresOn instanceof Date)) { + throw new ServerError('Server error: `refreshTokenExpiresOn` must be a Date instance'); + } + + if (token.refreshTokenExpiresOn >= new Date()) { + throw new ServerError('Server error: authorization code should be expired'); + } + + return token; + }); +}); + +/** + * Save token. + */ + +RefreshTokenGrantType.prototype.saveToken = function(user, client, scope) { + var fns = [ + this.generateAccessToken(), + this.generateRefreshToken(), + this.getAccessTokenExpiresOn(), + this.getRefreshTokenExpiresOn() + ]; + + return Promise.all(fns) + .bind(this) + .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn) { + var token = { + accessToken: accessToken, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshToken: refreshToken, + refreshTokenExpiresOn: refreshTokenExpiresOn, + scope: scope + }; + + return this.model.saveToken(token, client, user); }); }; diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index d2c696f15..bdad40bbb 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -7,6 +7,7 @@ var InvalidArgumentError = require('../errors/invalid-argument-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var InvalidScopeError = require('../errors/invalid-scope-error'); var InvalidTokenError = require('../errors/invalid-token-error'); +var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); var Request = require('../request'); var ServerError = require('../errors/server-error'); @@ -23,11 +24,11 @@ function AuthenticateHandler(options) { } if (!options.model.getAccessToken) { - throw new ServerError('Server error: model does not implement `getAccessToken()`'); + throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`'); } if (options.scope && !options.model.validateScope) { - throw new ServerError('Server error: model does not implement `validateScope()`'); + throw new InvalidArgumentError('Invalid argument: model does not implement `validateScope()`'); } this.model = options.model; @@ -43,16 +44,25 @@ AuthenticateHandler.prototype.handle = function(request) { throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); } - return this.getToken(request) - .bind(this) - .then(this.getAccessToken) - .then(function(accessToken) { - return this.validateAccessToken(accessToken) - .bind(this) - .then(this.validateScope) - .then(function() { - return accessToken; - }); + return Promise.bind(this) + .then(function() { + return this.getToken(request); + }) + .then(function(token) { + return this.getAccessToken(token); + }) + .tap(function(token) { + return this.validateAccessToken(token); + }) + .tap(function(token) { + return this.validateScope(token); + }) + .catch(function(e) { + if (!(e instanceof OAuthError)) { + throw new ServerError(e); + } + + throw e; }); }; diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index d694ca31c..7edab14da 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -44,11 +44,11 @@ function AuthorizeHandler(options) { } if (!options.model.getClient) { - throw new ServerError('Server error: model does not implement `getClient()`'); + throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); } if (!options.model.saveAuthCode) { - throw new ServerError('Server error: model does not implement `saveAuthCode()`'); + throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthCode()`'); } this.authCodeLifetime = options.authCodeLifetime; diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 52064b502..80c5c9928 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -7,18 +7,17 @@ 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 InvalidGrantError = require('../errors/invalid-grant-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); 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'); -var tokenUtil = require('../utils/token-util'); /** * Grant types. @@ -51,11 +50,7 @@ function TokenHandler(options) { } if (!options.model.getClient) { - throw new ServerError('Server error: model does not implement `getClient()`'); - } - - if (!options.model.saveToken) { - throw new ServerError('Server error: model does not implement `saveToken()`'); + throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); } this.accessTokenLifetime = options.accessTokenLifetime; @@ -90,32 +85,13 @@ TokenHandler.prototype.handle = function(request, response) { return this.getClient(request, response); }) .then(function(client) { - return this.handleGrantType(request, client) - .bind(this) - .then(function(instance) { - var fns = [ - this.generateAccessToken(), - this.generateRefreshToken(client), - this.getAccessTokenExpiresOn(), - this.getRefreshTokenExpiresOn(client), - this.getScope(request), - this.getUser(request, instance) - ]; - - return Promise.all(fns) - .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, user) { - return this.saveToken(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client, user) - .bind(this) - .then(function(token) { - var tokenType = this.getTokenType(accessToken, refreshToken, scope); - - this.updateSuccessResponse(response, tokenType); - - return token; - }); - }); - }); + return this.handleGrantType(request, client); + }) + .tap(function(data) { + var model = new TokenModel(data); + var tokenType = this.getTokenType(model); + + this.updateSuccessResponse(response, tokenType); }).catch(function(e) { if (!(e instanceof OAuthError)) { e = new ServerError(e); @@ -127,46 +103,6 @@ TokenHandler.prototype.handle = function(request, response) { }); }; -/** - * Generate access token. - */ - -TokenHandler.prototype.generateAccessToken = Promise.method(function() { - if (this.model.generateAccessToken) { - return this.model.generateAccessToken(); - } - - return tokenUtil.generateRandomToken(); -}); - -/** - * Generate refresh token. - */ - -TokenHandler.prototype.generateRefreshToken = Promise.method(function(client) { - if (!_.contains(client.grants, 'refresh_token')) { - return; - } - - if (this.model.generateRefreshToken) { - return this.model.generateRefreshToken(); - } - - return tokenUtil.generateRandomToken(); -}); - -/** - * Get access token expires on. - */ - -TokenHandler.prototype.getAccessTokenExpiresOn = Promise.method(function() { - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); - - return expires; -}); - /** * Get the client from the model. */ @@ -221,34 +157,6 @@ TokenHandler.prototype.getClient = Promise.method(function(request, response) { }); }); -/** - * Get refresh token expires on. - */ - -TokenHandler.prototype.getRefreshTokenExpiresOn = Promise.method(function(client) { - if (!_.contains(client.grants, 'refresh_token')) { - return; - } - - var expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); - - return expires; -}); - -/** - * Get scope from the request body. - */ - -TokenHandler.prototype.getScope = Promise.method(function(request) { - if (!is.nqschar(request.body.scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); - } - - return request.body.scope; -}); - /** * Get client credentials. * @@ -296,59 +204,18 @@ TokenHandler.prototype.handleGrantType = Promise.method(function(request, client } var Type = this.grantTypes[grantType]; + var options = { accessTokenLifetime: this.accessTokenLifetime, model: this.model, refreshTokenLifetime: this.refreshTokenLifetime }; - return new Type(this.model) - .handle(request, client) - .catch(function(e) { - if (!(e instanceof OAuthError)) { - throw new InvalidGrantError('Invalid grant: credentials are invalid'); - } - - throw e; - }); -}); - -/** - * Get user. - */ - -TokenHandler.prototype.getUser = Promise.method(function(request, instance) { - if ('authorization_code' === request.body.grant_type) { - return instance.user; - } - - if ('refresh_token' === request.body.grant_type) { - return instance.user; - } - - return instance; -}); - -/** - * Save token. - */ - -TokenHandler.prototype.saveToken = Promise.method(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn, scope, client, user) { - var token = { - accessToken: accessToken, - accessTokenExpiresOn: accessTokenExpiresOn, - scope: scope - }; - - if (refreshToken) { - token.refreshToken = refreshToken; - token.refreshTokenExpiresOn = refreshTokenExpiresOn; - } - - return this.model.saveToken(token, client, user); + return new Type(options) + .handle(request, client); }); /** * Get token type. */ -TokenHandler.prototype.getTokenType = function(accessToken, refreshToken, scope) { - return new BearerTokenType(accessToken, this.accessTokenLifetime, refreshToken, scope); +TokenHandler.prototype.getTokenType = function(model) { + return new BearerTokenType(model.accessToken, this.accessTokenLifetime, model.refreshToken, model.scope); }; /** diff --git a/lib/models/token-model.js b/lib/models/token-model.js new file mode 100644 index 000000000..9e8f95927 --- /dev/null +++ b/lib/models/token-model.js @@ -0,0 +1,48 @@ + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); + +/** + * Constructor. + */ + +function TokenModel(data) { + 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.accessTokenExpiresOn && !(data.accessTokenExpiresOn instanceof Date)) { + throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresOn`'); + } + + if (data.refreshTokenExpiresOn && !(data.refreshTokenExpiresOn instanceof Date)) { + throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresOn`'); + } + + this.accessToken = data.accessToken; + this.accessTokenExpiresOn = data.accessTokenExpiresOn; + this.client = data.client; + this.refreshToken = data.refreshToken; + this.refreshTokenExpiresOn = data.refreshTokenExpiresOn; + this.scope = data.scope; + this.user = data.user; +} + +/** + * Export constructor. + */ + +module.exports = TokenModel; diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js new file mode 100644 index 000000000..d169e33d3 --- /dev/null +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -0,0 +1,173 @@ + +/** + * 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('getAccessTokenExpiresOn()', function() { + it('should return a date', function() { + var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); + + handler.getAccessTokenExpiresOn().should.be.an.instanceOf(Date); + }); + }); + + describe('getRefreshTokenExpiresOn()', function() { + it('should return a refresh token', function() { + var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); + + handler.getRefreshTokenExpiresOn().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_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 353a021e4..315288474 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -10,7 +10,6 @@ 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 sinon = require('sinon'); var should = require('should'); /** @@ -32,28 +31,55 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if the model does not implement `getAuthCode()`', function() { try { - new AuthorizationCodeGrantType({}); + new AuthorizationCodeGrantType({ model: {} }); should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getAuthCode()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getAuthCode()`'); } }); - it('should set the `model`', function() { - var model = { - getAuthCode: function() {} - }; - var grantType = new AuthorizationCodeGrantType(model); + it('should throw an error if the model does not implement `revokeAuthCode()`', function() { + try { + var model = { + getAuthCode: function() {} + }; - grantType.model.should.equal(model); + new AuthorizationCodeGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `revokeAuthCode()`'); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', function() { + try { + var model = { + getAuthCode: function() {}, + revokeAuthCode: 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 grantType = new AuthorizationCodeGrantType({ getAuthCode: function() {} }); + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); try { grantType.handle(); @@ -68,11 +94,11 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `client` is missing', function() { var client = {}; var model = { - getAuthCode: function() { - return Promise.resolve({}); - } + getAuthCode: function() { return { authCode: 12345, expiresOn: new Date(new Date() * 2), user: {} }; }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); return grantType.handle(request, client) @@ -83,12 +109,63 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); + it('should return a token', function() { + var client = { id: 'foobar' }; + var token = {}; + var model = { + getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; }, + revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }; }, + saveToken: function() { return token; } + }; + 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 = { + getAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }); }, + revokeAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }) }, + 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 = { + getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; }, + revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }; }, + 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); + }); + }); + + describe('getAuthCode()', function() { it('should throw an error if the request body does not contain `code`', function() { var client = {}; - var grantType = new AuthorizationCodeGrantType({ getAuthCode: function() {} }); + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -98,10 +175,15 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `code` is invalid', function() { var client = {}; - var grantType = new AuthorizationCodeGrantType({ getAuthCode: function() {} }); + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -112,14 +194,14 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authCode` is missing', function() { var client = {}; var model = { - getAuthCode: function() { - return Promise.resolve(); - } + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -130,14 +212,14 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `authCode.client` is missing', function() { var client = {}; var model = { - getAuthCode: function() { - return Promise.resolve({}); - } + getAuthCode: function() { return { authCode: 12345 }; }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); @@ -145,17 +227,35 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); + it('should throw an error if `authCode.expiresOn` is missing', function() { + var client = {}; + var model = { + getAuthCode: function() { return { authCode: 12345, client: {}, user: {} }; }, + revokeAuthCode: function() {}, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); + + return grantType.getAuthCode(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `expiresOn` must be a Date instance'); + }); + }); + it('should throw an error if `authCode.user` is missing', function() { var client = {}; var model = { - getAuthCode: function() { - return Promise.resolve({ client: {} }); - } + getAuthCode: function() { return { authCode: 12345, client: {}, expiresOn: new Date() }; }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); @@ -167,13 +267,15 @@ describe('AuthorizationCodeGrantType integration', function() { var client = { id: 123 }; var model = { getAuthCode: function() { - return { client: { id: 456 }, user: {} }; - } + return { authCode: 12345, expiresOn: new Date(), client: { id: 456 }, user: {} }; + }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -186,13 +288,15 @@ describe('AuthorizationCodeGrantType integration', function() { var date = new Date(new Date() / 2); var model = { getAuthCode: function() { - return Promise.resolve({ client: { id: 123 }, expiresOn: date, user: {} }); - } + return { authCode: 12345, client: { id: 123 }, expiresOn: date, user: {} }; + }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -204,12 +308,14 @@ describe('AuthorizationCodeGrantType integration', function() { var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { - getAuthCode: sinon.stub().returns(authCode) + getAuthCode: function() { return authCode; }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getAuthCode(request, client) .then(function(data) { data.should.equal(authCode); }) @@ -220,28 +326,112 @@ describe('AuthorizationCodeGrantType integration', function() { var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { - getAuthCode: function() { - return Promise.resolve(authCode); - } + getAuthCode: function() { return Promise.resolve(authCode); }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + 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); + grantType.getAuthCode(request, client).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { - getAuthCode: function() { - return authCode; - } + getAuthCode: function() { return authCode; }, + revokeAuthCode: function() {}, + saveToken: function() {} }; - var grantType = new AuthorizationCodeGrantType(model); + 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); + grantType.getAuthCode(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe('revokeAuthCode()', function() { + it('should revoke the auth code', function() { + var authCode = { authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }; + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() { return authCode; }, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + + return grantType.revokeAuthCode(authCode) + .then(function(data) { + data.should.equal(authCode); + }) + .catch(should.fail); + }); + + it('should support promises', function() { + var authCode = { authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }; + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() { return Promise.resolve(authCode); }, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + + grantType.revokeAuthCode(authCode).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var authCode = { authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }; + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() { return authCode; }, + saveToken: function() {} + }; + var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); + + grantType.revokeAuthCode(authCode).should.be.an.instanceOf(Promise); + }); + }); + + describe('saveToken()', function() { + it('should save the token', function() { + var token = {}; + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: function() { return token; } + }; + 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 = { + getAuthCode: function() {}, + revokeAuthCode: 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 = { + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: function() { return 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_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index cf483bf95..a388fa37e 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -8,8 +8,6 @@ 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 ServerError = require('../../../lib/errors/server-error'); -var sinon = require('sinon'); var should = require('should'); /** @@ -31,28 +29,38 @@ describe('ClientCredentialsGrantType integration', function() { it('should throw an error if the model does not implement `getUserFromClient()`', function() { try { - new ClientCredentialsGrantType({}); + new ClientCredentialsGrantType({ model: {} }); should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getUserFromClient()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getUserFromClient()`'); } }); - it('should set the `model`', function() { - var model = { - getUserFromClient: function() {} - }; - var grantType = new ClientCredentialsGrantType(model); + it('should throw an error if the model does not implement `saveToken()`', function() { + try { + var model = { + getUserFromClient: function() {} + }; - grantType.model.should.equal(model); + 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 grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); + var model = { + getUserFromClient: function() {}, + saveToken: function() {} + }; + var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); try { grantType.handle(); @@ -65,7 +73,11 @@ describe('ClientCredentialsGrantType integration', function() { }); it('should throw an error if `client` is missing', function() { - var grantType = new ClientCredentialsGrantType({ getUserFromClient: function() {} }); + var model = { + getUserFromClient: function() {}, + saveToken: function() {} + }; + var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -78,14 +90,57 @@ describe('ClientCredentialsGrantType integration', function() { } }); - it('should throw an error if `user` is missing', function() { + it('should return a token', function() { + var token = {}; var model = { - getUserFromClient: function() {} + getUserFromClient: function() { return {}; }, + saveToken: function() { return token; } }; - var grantType = new ClientCredentialsGrantType(model); + 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); @@ -96,42 +151,80 @@ describe('ClientCredentialsGrantType integration', function() { it('should return a user', function() { var user = { email: 'foo@bar.com' }; var model = { - getUserFromClient: sinon.stub().returns(user) + getUserFromClient: function() { return user; }, + saveToken: function() {} }; - var grantType = new ClientCredentialsGrantType(model); + var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.handle(request, {}) + return grantType.getUserFromClient(request, {}) .then(function(data) { data.should.equal(user); }) .catch(should.fail); }); - it('should support promises when calling `model.getUserFromClient()`', function() { + it('should support promises', function() { var user = { email: 'foo@bar.com' }; var model = { - getUserFromClient: function() { - return Promise.resolve(user); - } + getUserFromClient: function() { return Promise.resolve(user); }, + saveToken: function() {} }; - var grantType = new ClientCredentialsGrantType(model); + var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - grantType.handle(request, {}).should.be.an.instanceOf(Promise); + grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); }); - it('should support non-promises when calling `model.getUserFromClient()`', function() { + it('should support non-promises', function() { var user = { email: 'foo@bar.com' }; var model = { - getUserFromClient: function() { - return user; - } + getUserFromClient: function() {return user; }, + saveToken: function() {} }; - var grantType = new ClientCredentialsGrantType(model); + var grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - grantType.handle(request, {}).should.be.an.instanceOf(Promise); + 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; } + }; + 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); }); }); }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index bc3a8ed82..f4f183b4d 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -9,8 +9,6 @@ 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 ServerError = require('../../../lib/errors/server-error'); -var sinon = require('sinon'); var should = require('should'); /** @@ -32,26 +30,38 @@ describe('PasswordGrantType integration', function() { it('should throw an error if the model does not implement `getUser()`', function() { try { - new PasswordGrantType({}); + new PasswordGrantType({ model: {} }); should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getUser()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getUser()`'); } }); - it('should set the `model`', function() { - var model = { getUser: function() {} }; - var grantType = new PasswordGrantType(model); + it('should throw an error if the model does not implement `saveToken()`', function() { + try { + var model = { + getUser: function() {} + }; + + new PasswordGrantType({ model: model }); - grantType.model.should.equal(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 grantType = new PasswordGrantType({ getUser: function() {} }); + var model = { + getUser: function() {}, + saveToken: function() {} + }; + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); try { grantType.handle(); @@ -63,11 +73,77 @@ describe('PasswordGrantType integration', function() { } }); + 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; } + }; + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); + var request = new Request({ body: { username: 'foo', password: 'bar' }, 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); + }); + }); + + describe('getUser()', function() { it('should throw an error if the request body does not contain `username`', function() { - var grantType = new PasswordGrantType({ getUser: function() {} }); + var model = { + getUser: function() {}, + saveToken: function() {} + }; + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.handle(request) + return grantType.getUser(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -76,10 +152,14 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if the request body does not contain `password`', function() { - var grantType = new PasswordGrantType({ getUser: 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: {} }); - return grantType.handle(request) + return grantType.getUser(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -88,10 +168,14 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if `username` is invalid', function() { - var grantType = new PasswordGrantType({ getUser: function() {} }); + var model = { + getUser: function() {}, + saveToken: function() {} + }; + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'ø倣‰', password: 'foobar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request) + return grantType.getUser(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -100,10 +184,14 @@ describe('PasswordGrantType integration', function() { }); it('should throw an error if `password` is invalid', function() { - var grantType = new PasswordGrantType({ getUser: function() {} }); + var model = { + getUser: function() {}, + saveToken: function() {} + }; + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foobar', password: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request) + return grantType.getUser(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -113,14 +201,13 @@ describe('PasswordGrantType integration', function() { it('should throw an error if `user` is missing', function() { var model = { - getUser: function() { - return Promise.resolve(); - } + getUser: function() {}, + saveToken: function() {} }; - var grantType = new PasswordGrantType(model); + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request) + return grantType.getUser(request) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -131,42 +218,80 @@ describe('PasswordGrantType integration', function() { it('should return a user', function() { var user = { email: 'foo@bar.com' }; var model = { - getUser: sinon.stub().returns(user) + getUser: function() { return user; }, + saveToken: function() {} }; - var grantType = new PasswordGrantType(model); + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request) + return grantType.getUser(request) .then(function(data) { data.should.equal(user); }) .catch(should.fail); }); - it('should support promises when calling `model.getUser()`', function() { + it('should support promises', function() { var user = { email: 'foo@bar.com' }; var model = { - getUser: function() { - return Promise.resolve(user); - } + getUser: function() { return Promise.resolve(user); }, + saveToken: function() {} }; - var grantType = new PasswordGrantType(model); + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - grantType.handle(request).should.be.an.instanceOf(Promise); + grantType.getUser(request).should.be.an.instanceOf(Promise); }); - it('should support non-promises when calling `model.getUser()`', function() { + it('should support non-promises', function() { var user = { email: 'foo@bar.com' }; var model = { - getUser: function() { - return user; - } + getUser: function() { return user; }, + saveToken: function() {} }; - var grantType = new PasswordGrantType(model); + var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - grantType.handle(request).should.be.an.instanceOf(Promise); + 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; } + }; + 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); }); }); }); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 7e7834611..a6f25e787 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -6,11 +6,10 @@ 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 RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); 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 sinon = require('sinon'); var should = require('should'); /** @@ -32,26 +31,55 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the model does not implement `getRefreshToken()`', function() { try { - new RefreshTokenGrantType({}); + new RefreshTokenGrantType({ model: {} }); should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getRefreshToken()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getRefreshToken()`'); } }); - it('should set the `model`', function() { - var model = { getRefreshToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); + it('should throw an error if the model does not implement `revokeToken()`', function() { + try { + var model = { + getRefreshToken: function() {} + }; + + new RefreshTokenGrantType({ model: model }); - grantType.model.should.equal(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 grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + var model = { + getRefreshToken: function() {}, + revokeToken: function() {}, + saveToken: function() {} + }; + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); try { grantType.handle(); @@ -63,17 +91,136 @@ describe('RefreshTokenGrantType integration', function() { } }); + 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 }, refreshTokenExpiresOn: 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: {}, refreshTokenExpiresOn: 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: {}, refreshTokenExpiresOn: 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); + }); + }); + + describe('getRefreshToken()', function() { + it('should throw an error if the requested `refreshToken` is missing', 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: {} }); + + return grantType.getRefreshToken(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `refresh_token`'); + }); + }); + + it('should throw an error if the requested `refreshToken` is invalid', function() { + var client = {}; + var model = { + getRefreshToken: function() {}, + revokeToken: function() {}, + saveToken: function() {} + }; + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + var request = new Request({ body: { refresh_token: [] }, headers: {}, method: {}, query: {} }); + + return grantType.getRefreshToken(request, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `refresh_token`'); + }); + }); + + it('should throw an error if `refreshToken` is missing', function() { + var client = {}; + var model = { + getRefreshToken: function() { return { accessToken: 'foo', client: { id: 123 }, 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 `refreshToken.client` is missing', function() { var client = {}; var model = { - getRefreshToken: function() { - return Promise.resolve({ expires: new Date() * 10 }); - } + getRefreshToken: function() { return {}; }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); @@ -85,13 +232,15 @@ describe('RefreshTokenGrantType integration', function() { var client = {}; var model = { getRefreshToken: function() { - return Promise.resolve({ client: {}, expires: new Date() * 10 }); - } + return { accessToken: 'foo', client: {} }; + }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); @@ -103,13 +252,15 @@ describe('RefreshTokenGrantType integration', function() { var client = { id: 123 }; var model = { getRefreshToken: function() { - return { client: { id: 456 }, user: {} }; - } + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -119,10 +270,17 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if the request body does not contain `refresh_token`', function() { var client = {}; - var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + 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: {}, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -132,10 +290,17 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refresh_token` is invalid', function() { var client = {}; - var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + 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: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidRequestError); @@ -145,10 +310,17 @@ describe('RefreshTokenGrantType integration', function() { it('should throw an error if `refresh_token` is missing', function() { var client = {}; - var grantType = new RefreshTokenGrantType({ getRefreshToken: function() {} }); + 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.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -157,17 +329,19 @@ describe('RefreshTokenGrantType integration', function() { }); it('should throw an error if `refresh_token` is expired', function() { - var client = {}; + var client = { id: 123 }; var date = new Date(new Date() / 2); var model = { getRefreshToken: function() { - return Promise.resolve({ client: {}, expires: date, user: {} }); - } + return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresOn: date, user: {} }; + }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -175,49 +349,134 @@ describe('RefreshTokenGrantType integration', function() { }); }); - it('should return a refresh token', function() { - var client = {}; - var date = new Date(new Date() * 2); - var refreshToken = { client: {}, expires: date, user: {} }; + it('should return a token', function() { + var client = { id: 123 }; + var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; var model = { - getRefreshToken: sinon.stub().returns(refreshToken) + getRefreshToken: function() { return token; }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model: model }); + var request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - return grantType.handle(request, client) + return grantType.getRefreshToken(request, client) .then(function(data) { - data.should.equal(refreshToken); + data.should.equal(token); }) .catch(should.fail); }); - it('should support promises when calling `model.getRefreshToken()`', function() { - var client = {}; - var refreshToken = { client: {}, user: {} }; + it('should support promises', function() { + var client = { id: 123 }; + var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; var model = { - getRefreshToken: function() { - return Promise.resolve(refreshToken); - } + getRefreshToken: function() { return Promise.resolve(token); }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + 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); + grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); }); - it('should support non-promises when calling `model.getRefreshToken()`', function() { - var client = {}; - var refreshToken = { client: {}, user: {} }; + it('should support non-promises', function() { + var client = { id: 123 }; + var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; var model = { - getRefreshToken: function() { - return refreshToken; - } + getRefreshToken: function() { return token; }, + revokeToken: function() {}, + saveToken: function() {} }; - var grantType = new RefreshTokenGrantType(model); - var request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); + 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); + grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe('revokeToken()', function() { + it('should revoke the token', function() { + var token = { accessToken: 'foo', client: {}, refreshTokenExpiresOn: 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: {}, refreshTokenExpiresOn: 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: {}, refreshTokenExpiresOn: 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); + }); + }); + + 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); }); }); }); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index b5164f9db..28ce81917 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -3,6 +3,7 @@ * 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'); @@ -36,8 +37,8 @@ describe('AuthenticateHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getAccessToken()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); } }); @@ -47,8 +48,8 @@ describe('AuthenticateHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `validateScope()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `validateScope()`'); } }); @@ -84,6 +85,40 @@ describe('AuthenticateHandler integration', function() { } }); + 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: {} }); + + return handler.handle(request) + .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: {} }); + + return handler.handle(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + }); + }); + it('should return an access token', function() { var accessToken = { user: {} }; var model = { diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index d3ef84ade..87d1c9f42 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -52,8 +52,8 @@ describe('AuthorizeHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getClient()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getClient()`'); } }); @@ -63,8 +63,8 @@ describe('AuthorizeHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `saveAuthCode()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `saveAuthCode()`'); } }); @@ -79,8 +79,8 @@ describe('AuthorizeHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getAccessToken()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); } }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 27e8507cb..ea012ee02 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -65,23 +65,8 @@ describe('TokenHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `getClient()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - var model = { - getClient: function() {} - }; - - try { - new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: model does not implement `saveToken()`'); + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getClient()`'); } }); @@ -219,14 +204,10 @@ describe('TokenHandler integration', function() { it('should throw a server error if a non-oauth error is thrown', function() { var model = { getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { throw new Error('Unhandled exception'); - } + }, + getUser: function() {}, + saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -255,14 +236,10 @@ describe('TokenHandler integration', function() { it('should update the response if an error is thrown', function() { var model = { getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { throw new Error('Unhandled exception'); - } + }, + getUser: function() {}, + saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -288,17 +265,11 @@ describe('TokenHandler integration', function() { }); it('should return a bearer token if successful', function() { - var token = { accessToken: 'foo', refreshToken: 'bar', accessTokenLifetime: 120, scope: 'foobar' }; + var token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: 'foobar', user: {} }; var model = { - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return token; - } + getClient: function() { return { grants: ['password'] }; }, + getUser: function() { return {}; }, + saveToken: function() { return token; } }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -323,104 +294,6 @@ describe('TokenHandler integration', function() { }); }); - describe('generateAccessToken()', function() { - it('should return an access token', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - 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({}); - }, - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - generateAccessToken: function() { - return {}; - }, - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.generateAccessToken().should.be.an.instanceOf(Promise); - }); - }); - - describe('generateRefreshToken()', function() { - describe('if the client does not support the `refresh_token` grant', function() { - it('should not return a refresh token', function *() { - var client = { - grants: [] - }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.generateRefreshToken(client) - .then(function(data) { - should.not.exist(data); - }) - .catch(should.fail); - }); - }); - - describe('if the client supports the `refresh_token` grant', function() { - it('should return a refresh token', function() { - var client = { - grants: ['refresh_token'] - }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.generateRefreshToken(client) - .then(function(data) { - data.should.be.a.sha1; - }) - .catch(should.fail); - }); - }); - }); - - describe('getAccessTokenExpiresOn()', function() { - it('should return a date', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.getAccessTokenExpiresOn() - .then(function(data) { - data.should.be.an.instanceOf(Date); - }) - .catch(should.fail); - }); - }); - describe('getClient()', function() { it('should throw an error if `clientId` is invalid', function() { var model = { @@ -472,9 +345,7 @@ describe('TokenHandler integration', function() { it('should throw an error if `client.grants` is missing', function() { var model = { - getClient: function() { - return {}; - }, + getClient: function() { return {}; }, saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -490,9 +361,7 @@ describe('TokenHandler integration', function() { it('should throw an error if `client.grants` is invalid', function() { var model = { - getClient: function() { - return { grants: 'foobar' }; - }, + getClient: function() { return { grants: 'foobar' }; }, saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -534,9 +403,7 @@ describe('TokenHandler integration', function() { it('should return a client', function() { var client = { id: 12345, grants: [] }; var model = { - getClient: function() { - return client; - }, + getClient: function() { return client; }, saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -551,9 +418,7 @@ describe('TokenHandler integration', function() { it('should support promises', function() { var model = { - getClient: function() { - return Promise.resolve({ grants: [] }); - }, + getClient: function() { return Promise.resolve({ grants: [] }); }, saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -564,9 +429,7 @@ describe('TokenHandler integration', function() { it('should support non-promises', function() { var model = { - getClient: function() { - return { grants: [] }; - }, + getClient: function() { return { grants: [] }; }, saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); @@ -576,79 +439,6 @@ describe('TokenHandler integration', function() { }); }); - describe('getRefreshTokenExpiresOn()', function() { - describe('if the client does not support the `refresh_token` grant', function() { - it('should not return a refresh token', function *() { - var client = { - grants: [''] - }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.getRefreshTokenExpiresOn(client) - .then(function(data) { - should.not.exist(data); - }) - .catch(should.fail); - }); - }); - - describe('if the client supports the `refresh_token` grant', function() { - it('should return a refresh token', function() { - var client = { - grants: ['refresh_token'] - }; - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.getRefreshTokenExpiresOn(client) - .then(function(data) { - data.should.be.an.instanceOf(Date); - }) - .catch(should.fail); - }); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - - return handler.getScope(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `scope`'); - }); - }); - - it('should return the scope', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - return handler.getScope(request) - .then(function(scope) { - scope.should.equal('foo'); - }) - .catch(should.fail); - }); - }); - describe('getClientCredentials()', function() { it('should throw an error if `client_id` is missing', function() { var model = { @@ -806,114 +596,15 @@ describe('TokenHandler integration', function() { }); }); - it('should return a grant type result if the `grant_type` is a uri', function() { - var client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; - var user = {}; - var model = { - getClient: function() {}, - getUser: function() { - return user; - }, - saveToken: function() {} - }; - 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(user); - }) - .catch(should.fail); - }); - - it('should return a grant type result if the `grant_type` is not a uri', function() { - var client = { grants: ['password'] }; - var user = {}; - var model = { - getClient: function() {}, - getUser: function() { - return user; - }, - 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(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should set `refreshToken` if `refreshToken` is defined', function() { - var model = { - getClient: function() {}, - saveToken: function(token) { - token.should.have.properties('refreshToken', 'refreshTokenExpiresOn'); - } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz') - .catch(should.fail); - }); - - it('should return a token', function() { - var token = {}; - var model = { - getClient: function() {}, - saveToken: function() { - return token; - } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz') - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var model = { - getClient: function() {}, - saveToken: function() { - return Promise.resolve({}); - } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var model = { - getClient: function() {}, - saveToken: function() { - return {}; - } - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.saveToken('foo', 'bar', 'biz', 'baz', 'qux', 'fuz').should.be.an.instanceOf(Promise); - }); - }); - - describe('handleGrantType()', function() { describe('with grant_type `authorization_code`', function() { - it('should return a user', function() { - var authCode = { client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; + it('should return a token', function() { var client = { id: 'foobar', grants: ['authorization_code'] }; + var token = {}; var model = { - getAuthCode: function() { - return authCode; - }, + getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; }, getClient: function() {}, - saveToken: function() {} + saveToken: function() { return token; }, + revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }; } }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -928,22 +619,20 @@ describe('TokenHandler integration', function() { return handler.handleGrantType(request, client) .then(function(data) { - data.should.equal(authCode); + data.should.equal(token); }) .catch(should.fail); }); }); describe('with grant_type `client_credentials`', function() { - it('should return a user', function() { + it('should return a token', function() { var client = { grants: ['client_credentials'] }; - var user = {}; + var token = {}; var model = { getClient: function() {}, - getUserFromClient: function() { - return user; - }, - saveToken: function() {} + getUserFromClient: function() { return {}; }, + saveToken: function() { return token; } }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -957,22 +646,20 @@ describe('TokenHandler integration', function() { return handler.handleGrantType(request, client) .then(function(data) { - data.should.equal(user); + data.should.equal(token); }) .catch(should.fail); }); }); describe('with grant_type `password`', function() { - it('should return a user', function() { + it('should return a token', function() { var client = { grants: ['password'] }; - var user = {}; + var token = {}; var model = { getClient: function() {}, - getUser: function() { - return user; - }, - saveToken: function() {} + getUser: function() { return {}; }, + saveToken: function() { return token; } }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -990,22 +677,21 @@ describe('TokenHandler integration', function() { return handler.handleGrantType(request, client) .then(function(data) { - data.should.equal(user); + data.should.equal(token); }) .catch(should.fail); }); }); describe('with grant_type `refresh_token`', function() { - it('should return a user', function() { + it('should return a token', function() { var client = { grants: ['refresh_token'] }; - var refreshToken = { client: {}, user: {} }; + var token = { accessToken: 'foo', client: {}, user: {} }; var model = { getClient: function() {}, - getRefreshToken: function() { - return refreshToken; - }, - saveToken: function() {} + getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() * 2), user: {} }; }, + saveToken: function() { return token; }, + revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }; } }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ @@ -1020,81 +706,27 @@ describe('TokenHandler integration', function() { return handler.handleGrantType(request, client) .then(function(data) { - data.should.equal(refreshToken); + data.should.equal(token); }) .catch(should.fail); }); }); - }); - describe('getUser()', function() { - describe('with grant_type `authorization_code`', function() { - it('should return a user', function() { + 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() {}, - saveToken: function() {} + getUser: function() { return {}; }, + saveToken: function() { return token; } }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'authorization_code' }, headers: {}, method: {}, query: {} }); - var user = {}; - - return handler.getUser(request, { user: user }) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - }); + 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: {} }); - describe('with grant_type `client_credentials`', function() { - it('should return a user', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'client_credentials' }, headers: {}, method: {}, query: {} }); - var result = {}; - - return handler.getUser(request, result) - .then(function(data) { - data.should.equal(result); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `password`', function() { - it('should return a user', function() { - 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: {} }); - var result = {}; - - return handler.getUser(request, result) - .then(function(data) { - data.should.equal(result); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `refresh_token`', function() { - it('should return a user', function() { - var model = { - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var request = new Request({ body: { grant_type: 'refresh_token' }, headers: {}, method: {}, query: {} }); - var user = {}; - - return handler.getUser(request, { user: user }) + return handler.handleGrantType(request, client) .then(function(data) { - data.should.equal(user); + data.should.equal(token); }) .catch(should.fail); }); @@ -1108,7 +740,7 @@ describe('TokenHandler integration', function() { saveToken: function() {} }; var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - var tokenType = handler.getTokenType('foo', 'bar', 'foobar'); + var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: 120, refreshToken: 'bar', scope: 'foobar' }); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 9a4218544..5dfd56a22 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -131,7 +131,7 @@ describe('Server integration', function() { return {}; }, saveToken: function() { - return { accessToken: 1234 }; + return { accessToken: 1234, client: {}, user: {} }; } }; var server = new Server({ model: model }); @@ -151,7 +151,7 @@ describe('Server integration', function() { return {}; }, saveToken: function() { - return { accessToken: 1234 }; + return { accessToken: 1234, client: {}, user: {} }; } }; var server = new Server({ model: model }); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js new file mode 100644 index 000000000..0bb79a6b3 --- /dev/null +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -0,0 +1,44 @@ + +/** + * 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: {}, expiresOn: new Date(), user: {} }) + }; + var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); + + return handler.generateAccessToken() + .then(function() { + model.generateAccessToken.callCount.should.equal(1); + }) + .catch(should.fail); + }); + }); + + describe('generateRefreshToken()', function() { + it('should call `model.generateRefreshToken()`', function() { + var model = { + generateRefreshToken: sinon.stub().returns({ client: {}, expiresOn: 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); + }) + .catch(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 new file mode 100644 index 000000000..4cd83327f --- /dev/null +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -0,0 +1,82 @@ + +/** + * 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('getAuthCode()', function() { + it('should call `model.getAuthCode()`', function() { + var model = { + getAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresOn: new Date(new Date() * 2), user: {} }), + revokeAuthCode: 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.getAuthCode(request, client) + .then(function() { + model.getAuthCode.callCount.should.equal(1); + model.getAuthCode.firstCall.args.should.have.length(1); + model.getAuthCode.firstCall.args[0].should.equal(12345); + }) + .catch(should.fail); + }); + }); + + describe('revokeAuthCode()', function() { + it('should call `model.revokeAuthCode()`', function() { + var model = { + getAuthCode: function() {}, + revokeAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }), + saveToken: function() {} + }; + var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); + var authCode = {}; + + return handler.revokeAuthCode(authCode) + .then(function() { + model.revokeAuthCode.callCount.should.equal(1); + model.revokeAuthCode.firstCall.args.should.have.length(1); + model.revokeAuthCode.firstCall.args[0].should.equal(authCode); + }) + .catch(should.fail); + }); + }); + + describe('saveToken()', function() { + it('should call `model.saveToken()`', function() { + var client = {}; + var user = {}; + var model = { + getAuthCode: function() {}, + revokeAuthCode: function() {}, + saveToken: sinon.stub().returns(true) + }; + var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); + + sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foo')); + + 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', authCode: 'foobar', scope: 'foobiz' }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + }) + .catch(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 new file mode 100644 index 000000000..5dcaa4cd1 --- /dev/null +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -0,0 +1,60 @@ + +/** + * 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); + }) + .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, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresOn').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresOn').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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js new file mode 100644 index 000000000..91a333fad --- /dev/null +++ b/test/unit/grant-types/password-grant-type_test.js @@ -0,0 +1,62 @@ + +/** + * 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'); + }) + .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, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresOn').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresOn').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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + }) + .catch(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 new file mode 100644 index 000000000..e09f8e326 --- /dev/null +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -0,0 +1,106 @@ + +/** + * 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: {}, refreshTokenExpiresOn: 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); + }) + .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'); + }) + .catch(should.fail); + }); + }); + + describe('revokeToken()', function() { + it('should call `model.revokeToken()`', function() { + var model = { + getRefreshToken: function() {}, + revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresOn: 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); + }) + .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, 'getAccessTokenExpiresOn').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresOn').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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index 3810dbea5..ba11e0169 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -13,45 +13,6 @@ var should = require('should'); */ describe('TokenHandler', function() { - describe('generateAccessToken()', function() { - it('should call `model.generateAccessToken()`', function() { - var model = { - generateAccessToken: sinon.spy(), - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.generateAccessToken() - .then(function() { - model.generateAccessToken.callCount.should.equal(1); - model.generateAccessToken.firstCall.args.should.have.length(0); - }) - .catch(should.fail); - }); - }); - - describe('generateRefreshToken()', function() { - it('should call `model.generateRefreshToken()`', function() { - var client = { - grants: ['refresh_token'] - }; - var model = { - generateRefreshToken: sinon.spy(), - getClient: function() {}, - saveToken: function() {} - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.generateRefreshToken(client) - .then(function() { - model.generateRefreshToken.callCount.should.equal(1); - model.generateRefreshToken.firstCall.args.should.have.length(0); - }) - .catch(should.fail); - }); - }); - describe('getClient()', function() { it('should call `model.getClient()`', function() { var model = { @@ -71,24 +32,4 @@ describe('TokenHandler', function() { .catch(should.fail); }); }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - var model = { - getClient: function() {}, - saveToken: sinon.stub().returns({}) - }; - var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - return handler.saveToken('foo', 'bar', 'biz', 'baz', 'fiz', 'qux', 'fuz') - .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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'fiz' }); - model.saveToken.firstCall.args[1].should.equal('qux'); - model.saveToken.firstCall.args[2].should.equal('fuz'); - }) - .catch(should.fail); - }); - }); }); From 12c630ddf3a70b8d25d4676927dae238592ee7ca Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 20 Apr 2015 13:09:32 +0100 Subject: [PATCH 15/39] Rename `expiresOn` to `expiresAt` --- lib/grant-types/abstract-grant-type.js | 4 +-- .../authorization-code-grant-type.js | 12 +++---- .../client-credentials-grant-type.js | 10 +++--- lib/grant-types/password-grant-type.js | 10 +++--- lib/grant-types/refresh-token-grant-type.js | 22 ++++++------ lib/handlers/authenticate-handler.js | 4 +-- lib/handlers/authorize-handler.js | 8 ++--- lib/models/token-model.js | 12 +++---- .../grant-types/abstract-grant-type_test.js | 8 ++--- .../authorization-code-grant-type_test.js | 36 +++++++++---------- .../refresh-token-grant-type_test.js | 14 ++++---- .../handlers/authenticate-handler_test.js | 2 +- .../handlers/token-handler_test.js | 8 ++--- .../grant-types/abstract-grant-type_test.js | 4 +-- .../authorization-code-grant-type_test.js | 4 +-- .../client-credentials-grant-type_test.js | 6 ++-- .../grant-types/password-grant-type_test.js | 6 ++-- .../refresh-token-grant-type_test.js | 10 +++--- test/unit/handlers/authorize-handler_test.js | 2 +- 19 files changed, 91 insertions(+), 91 deletions(-) diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index 5f85aba59..8aee4c310 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -56,7 +56,7 @@ AbstractGrantType.prototype.generateRefreshToken = Promise.method(function() { * Get access token expires on. */ -AbstractGrantType.prototype.getAccessTokenExpiresOn = function() { +AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { var expires = new Date(); expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime); @@ -68,7 +68,7 @@ AbstractGrantType.prototype.getAccessTokenExpiresOn = function() { * Get refresh token expires on. */ -AbstractGrantType.prototype.getRefreshTokenExpiresOn = function() { +AbstractGrantType.prototype.getRefreshTokenExpiresAt = function() { var expires = new Date(); expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime); diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 76afc3071..5546fe404 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -102,11 +102,11 @@ AuthCodeGrantType.prototype.getAuthCode = function(request, client) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (!(authCode.expiresOn instanceof Date)) { - throw new ServerError('Server error: `expiresOn` must be a Date instance'); + if (!(authCode.expiresAt instanceof Date)) { + throw new ServerError('Server error: `expiresAt` must be a Date instance'); } - if (authCode.expiresOn < new Date()) { + if (authCode.expiresAt < new Date()) { throw new InvalidGrantError('Invalid grant: authorization code has expired'); } @@ -131,11 +131,11 @@ AuthCodeGrantType.prototype.revokeAuthCode = Promise.method(function(authCode) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (!(authCode.expiresOn instanceof Date)) { - throw new ServerError('Server error: `expiresOn` must be a Date instance'); + if (!(authCode.expiresAt instanceof Date)) { + throw new ServerError('Server error: `expiresAt` must be a Date instance'); } - if (authCode.expiresOn >= new Date()) { + if (authCode.expiresAt >= new Date()) { throw new ServerError('Server error: authorization code should be expired'); } diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index b12de67d4..c43ff36be 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -86,18 +86,18 @@ ClientCredentialsGrantType.prototype.saveToken = function(user, client, scope) { var fns = [ this.generateAccessToken(), this.generateRefreshToken(), - this.getAccessTokenExpiresOn(), - this.getRefreshTokenExpiresOn() + this.getAccessTokenExpiresAt(), + this.getRefreshTokenExpiresAt() ]; return Promise.all(fns) .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn) { + .spread(function(accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { var token = { accessToken: accessToken, - accessTokenExpiresOn: accessTokenExpiresOn, + accessTokenExpiresAt: accessTokenExpiresAt, refreshToken: refreshToken, - refreshTokenExpiresOn: refreshTokenExpiresOn, + refreshTokenExpiresAt: refreshTokenExpiresAt, scope: scope }; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index cbf6e5a23..dbcad4b2b 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -104,18 +104,18 @@ PasswordGrantType.prototype.saveToken = function(user, client, scope) { var fns = [ this.generateAccessToken(), this.generateRefreshToken(), - this.getAccessTokenExpiresOn(), - this.getRefreshTokenExpiresOn() + this.getAccessTokenExpiresAt(), + this.getRefreshTokenExpiresAt() ]; return Promise.all(fns) .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn) { + .spread(function(accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { var token = { accessToken: accessToken, - accessTokenExpiresOn: accessTokenExpiresOn, + accessTokenExpiresAt: accessTokenExpiresAt, refreshToken: refreshToken, - refreshTokenExpiresOn: refreshTokenExpiresOn, + refreshTokenExpiresAt: refreshTokenExpiresAt, scope: scope }; diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 383d17030..8b6b48217 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -102,11 +102,11 @@ RefreshTokenGrantType.prototype.getRefreshToken = function(request, client) { throw new InvalidGrantError('Invalid grant: refresh token is invalid'); } - if (token.refreshTokenExpiresOn && !(token.refreshTokenExpiresOn instanceof Date)) { - throw new ServerError('Server error: `refreshTokenExpiresOn` must be a Date instance'); + if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { + throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); } - if (token.refreshTokenExpiresOn && token.refreshTokenExpiresOn < new Date()) { + if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { throw new InvalidGrantError('Invalid grant: refresh token has expired'); } @@ -127,11 +127,11 @@ RefreshTokenGrantType.prototype.revokeToken = Promise.method(function(token) { throw new InvalidGrantError('Invalid grant: refresh token is invalid'); } - if (!(token.refreshTokenExpiresOn instanceof Date)) { - throw new ServerError('Server error: `refreshTokenExpiresOn` must be a Date instance'); + if (!(token.refreshTokenExpiresAt instanceof Date)) { + throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); } - if (token.refreshTokenExpiresOn >= new Date()) { + if (token.refreshTokenExpiresAt >= new Date()) { throw new ServerError('Server error: authorization code should be expired'); } @@ -147,18 +147,18 @@ RefreshTokenGrantType.prototype.saveToken = function(user, client, scope) { var fns = [ this.generateAccessToken(), this.generateRefreshToken(), - this.getAccessTokenExpiresOn(), - this.getRefreshTokenExpiresOn() + this.getAccessTokenExpiresAt(), + this.getRefreshTokenExpiresAt() ]; return Promise.all(fns) .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn) { + .spread(function(accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { var token = { accessToken: accessToken, - accessTokenExpiresOn: accessTokenExpiresOn, + accessTokenExpiresAt: accessTokenExpiresAt, refreshToken: refreshToken, - refreshTokenExpiresOn: refreshTokenExpiresOn, + refreshTokenExpiresAt: refreshTokenExpiresAt, scope: scope }; diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index bdad40bbb..1ab0dba24 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -163,11 +163,11 @@ AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { */ AuthenticateHandler.prototype.validateAccessToken = Promise.method(function(accessToken) { - if (accessToken.accessTokenExpiresOn && !(accessToken.accessTokenExpiresOn instanceof Date)) { + if (accessToken.accessTokenExpiresAt && !(accessToken.accessTokenExpiresAt instanceof Date)) { throw new ServerError('Server error: `expires` must be a Date instance'); } - if (accessToken.accessTokenExpiresOn && accessToken.accessTokenExpiresOn < new Date()) { + if (accessToken.accessTokenExpiresAt && accessToken.accessTokenExpiresAt < new Date()) { throw new InvalidTokenError('Invalid token: access token has expired'); } diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 7edab14da..dd67ad87e 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -84,8 +84,8 @@ AuthorizeHandler.prototype.handle = function(request, response) { return Promise.all(fns) .bind(this) - .spread(function(authCode, expiresOn, client, scope, state, user) { - return this.saveAuthCode(authCode, expiresOn, scope, client, user) + .spread(function(authCode, expiresAt, client, scope, state, user) { + return this.saveAuthCode(authCode, expiresAt, scope, client, user) .bind(this) .then(function(code) { var responseType = this.getResponseType(request, code); @@ -224,10 +224,10 @@ AuthorizeHandler.prototype.getUser = Promise.method(function(request) { * Save auth code. */ -AuthorizeHandler.prototype.saveAuthCode = Promise.method(function(authCode, expiresOn, scope, client, user) { +AuthorizeHandler.prototype.saveAuthCode = Promise.method(function(authCode, expiresAt, scope, client, user) { var code = { authCode: authCode, - expiresOn: expiresOn, + expiresAt: expiresAt, scope: scope }; diff --git a/lib/models/token-model.js b/lib/models/token-model.js index 9e8f95927..333b6daa1 100644 --- a/lib/models/token-model.js +++ b/lib/models/token-model.js @@ -24,19 +24,19 @@ function TokenModel(data) { throw new InvalidArgumentError('Missing parameter: `user`'); } - if (data.accessTokenExpiresOn && !(data.accessTokenExpiresOn instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresOn`'); + if (data.accessTokenExpiresAt && !(data.accessTokenExpiresAt instanceof Date)) { + throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresAt`'); } - if (data.refreshTokenExpiresOn && !(data.refreshTokenExpiresOn instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresOn`'); + if (data.refreshTokenExpiresAt && !(data.refreshTokenExpiresAt instanceof Date)) { + throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresAt`'); } this.accessToken = data.accessToken; - this.accessTokenExpiresOn = data.accessTokenExpiresOn; + this.accessTokenExpiresAt = data.accessTokenExpiresAt; this.client = data.client; this.refreshToken = data.refreshToken; - this.refreshTokenExpiresOn = data.refreshTokenExpiresOn; + this.refreshTokenExpiresAt = data.refreshTokenExpiresAt; this.scope = data.scope; this.user = data.user; } diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index d169e33d3..a066160c5 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -125,19 +125,19 @@ describe('AbstractGrantType integration', function() { }); }); - describe('getAccessTokenExpiresOn()', function() { + describe('getAccessTokenExpiresAt()', function() { it('should return a date', function() { var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - handler.getAccessTokenExpiresOn().should.be.an.instanceOf(Date); + handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); }); }); - describe('getRefreshTokenExpiresOn()', function() { + describe('getRefreshTokenExpiresAt()', function() { it('should return a refresh token', function() { var handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - handler.getRefreshTokenExpiresOn().should.be.an.instanceOf(Date); + handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); }); }); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 315288474..28a7dde08 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -94,7 +94,7 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `client` is missing', function() { var client = {}; var model = { - getAuthCode: function() { return { authCode: 12345, expiresOn: new Date(new Date() * 2), user: {} }; }, + getAuthCode: function() { return { authCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, revokeAuthCode: function() {}, saveToken: function() {} }; @@ -113,8 +113,8 @@ describe('AuthorizationCodeGrantType integration', function() { var client = { id: 'foobar' }; var token = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; }, - revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }; }, + getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, + revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: function() { return token; } }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -130,8 +130,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }); }, - revokeAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }) }, + getAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, + revokeAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }) }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -143,8 +143,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support non-promises', function() { var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; }, - revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }; }, + getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, + revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -227,7 +227,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); - it('should throw an error if `authCode.expiresOn` is missing', function() { + it('should throw an error if `authCode.expiresAt` is missing', function() { var client = {}; var model = { getAuthCode: function() { return { authCode: 12345, client: {}, user: {} }; }, @@ -241,14 +241,14 @@ describe('AuthorizationCodeGrantType integration', function() { .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `expiresOn` must be a Date instance'); + e.message.should.equal('Server error: `expiresAt` must be a Date instance'); }); }); it('should throw an error if `authCode.user` is missing', function() { var client = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: {}, expiresOn: new Date() }; }, + getAuthCode: function() { return { authCode: 12345, client: {}, expiresAt: new Date() }; }, revokeAuthCode: function() {}, saveToken: function() {} }; @@ -267,7 +267,7 @@ describe('AuthorizationCodeGrantType integration', function() { var client = { id: 123 }; var model = { getAuthCode: function() { - return { authCode: 12345, expiresOn: new Date(), client: { id: 456 }, user: {} }; + return { authCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; }, revokeAuthCode: function() {}, saveToken: function() {} @@ -288,7 +288,7 @@ describe('AuthorizationCodeGrantType integration', function() { var date = new Date(new Date() / 2); var model = { getAuthCode: function() { - return { authCode: 12345, client: { id: 123 }, expiresOn: date, user: {} }; + return { authCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; }, revokeAuthCode: function() {}, saveToken: function() {} @@ -305,7 +305,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should return an auth code', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; + var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { getAuthCode: function() { return authCode; }, @@ -323,7 +323,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should support promises', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; + var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { getAuthCode: function() { return Promise.resolve(authCode); }, @@ -337,7 +337,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should support non-promises', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; + var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { getAuthCode: function() { return authCode; }, @@ -353,7 +353,7 @@ describe('AuthorizationCodeGrantType integration', function() { describe('revokeAuthCode()', function() { it('should revoke the auth code', function() { - var authCode = { authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }; + var authCode = { authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; var model = { getAuthCode: function() {}, revokeAuthCode: function() { return authCode; }, @@ -369,7 +369,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should support promises', function() { - var authCode = { authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }; + var authCode = { authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; var model = { getAuthCode: function() {}, revokeAuthCode: function() { return Promise.resolve(authCode); }, @@ -381,7 +381,7 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should support non-promises', function() { - var authCode = { authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }; + var authCode = { authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; var model = { getAuthCode: function() {}, revokeAuthCode: function() { return authCode; }, diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index a6f25e787..06ca0e8c8 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -115,7 +115,7 @@ describe('RefreshTokenGrantType integration', function() { var token = { accessToken: 'foo', client: { id: 123 }, user: {} }; var model = { getRefreshToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }; }, + 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 }); @@ -132,7 +132,7 @@ describe('RefreshTokenGrantType integration', 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: {}, refreshTokenExpiresOn: new Date(new Date() / 2), 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 }); @@ -145,7 +145,7 @@ describe('RefreshTokenGrantType integration', function() { var client = { id: 123 }; var model = { getRefreshToken: function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), 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 }); @@ -333,7 +333,7 @@ describe('RefreshTokenGrantType integration', function() { var date = new Date(new Date() / 2); var model = { getRefreshToken: function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresOn: date, user: {} }; + return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; }, revokeToken: function() {}, saveToken: function() {} @@ -398,7 +398,7 @@ describe('RefreshTokenGrantType integration', function() { describe('revokeToken()', function() { it('should revoke the token', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }; + var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; var model = { getRefreshToken: function() {}, revokeToken: function() { return token; }, @@ -414,7 +414,7 @@ describe('RefreshTokenGrantType integration', function() { }); it('should support promises', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }; + var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; var model = { getRefreshToken: function() {}, revokeToken: function() { return Promise.resolve(token); }, @@ -426,7 +426,7 @@ describe('RefreshTokenGrantType integration', function() { }); it('should support non-promises', function() { - var token = { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }; + var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; var model = { getRefreshToken: function() {}, revokeToken: function() { return token; }, diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 28ce81917..6d477b883 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -348,7 +348,7 @@ describe('AuthenticateHandler integration', function() { describe('validateAccessToken()', function() { it('should throw an error if `accessToken` is expired', function() { - var accessToken = { accessTokenExpiresOn: new Date(new Date() / 2) }; + var accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); return handler.validateAccessToken(accessToken) diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index ea012ee02..e71ccbb0a 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -601,10 +601,10 @@ describe('TokenHandler integration', function() { var client = { id: 'foobar', grants: ['authorization_code'] }; var token = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() * 2), user: {} }; }, + getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, getClient: function() {}, saveToken: function() { return token; }, - revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresOn: new Date(new Date() / 2), user: {} }; } + revokeAuthCode: function() { return { authCode: 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({ @@ -689,9 +689,9 @@ describe('TokenHandler integration', function() { var token = { accessToken: 'foo', client: {}, user: {} }; var model = { getClient: function() {}, - getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() * 2), user: {} }; }, + getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; }, saveToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }; } + 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({ diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index 0bb79a6b3..42ec4d5ad 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -15,7 +15,7 @@ describe('AbstractGrantType', function() { describe('generateAccessToken()', function() { it('should call `model.generateAccessToken()`', function() { var model = { - generateAccessToken: sinon.stub().returns({ client: {}, expiresOn: new Date(), user: {} }) + generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }) }; var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); @@ -30,7 +30,7 @@ describe('AbstractGrantType', function() { describe('generateRefreshToken()', function() { it('should call `model.generateRefreshToken()`', function() { var model = { - generateRefreshToken: sinon.stub().returns({ client: {}, expiresOn: new Date(new Date() / 2), user: {} }) + generateRefreshToken: sinon.stub().returns({ client: {}, expiresAt: new Date(new Date() / 2), user: {} }) }; var handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index 4cd83327f..5cdfa7bb5 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -17,7 +17,7 @@ describe('AuthorizationCodeGrantType', function() { describe('getAuthCode()', function() { it('should call `model.getAuthCode()`', function() { var model = { - getAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresOn: new Date(new Date() * 2), user: {} }), + getAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), user: {} }), revokeAuthCode: function() {}, saveToken: function() {} }; @@ -39,7 +39,7 @@ describe('AuthorizationCodeGrantType', function() { it('should call `model.revokeAuthCode()`', function() { var model = { getAuthCode: function() {}, - revokeAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresOn: new Date(new Date() / 2), user: {} }), + revokeAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }), saveToken: function() {} }; var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index 5dcaa4cd1..c68b46ac4 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -43,14 +43,14 @@ describe('ClientCredentialsGrantType', function() { sinon.stub(handler, 'generateAccessToken').returns('foo'); sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresOn').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresOn').returns('baz'); + 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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); }) diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js index 91a333fad..a3ae0299d 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -45,14 +45,14 @@ describe('PasswordGrantType', function() { sinon.stub(handler, 'generateAccessToken').returns('foo'); sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresOn').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresOn').returns('baz'); + 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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); }) diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index e09f8e326..1ebe8d13d 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -19,7 +19,7 @@ describe('RefreshTokenGrantType', function() { var model = { getRefreshToken: function() { return token; }, saveToken: function() { return { accessToken: 'bar', client: {}, user: {} }; }, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), 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: {} }); @@ -60,7 +60,7 @@ describe('RefreshTokenGrantType', function() { it('should call `model.revokeToken()`', function() { var model = { getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresOn: new Date(new Date() / 2), user: {} }), + revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), saveToken: function() {} }; var handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); @@ -89,14 +89,14 @@ describe('RefreshTokenGrantType', function() { sinon.stub(handler, 'generateAccessToken').returns('foo'); sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresOn').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresOn').returns('baz'); + 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', accessTokenExpiresOn: 'biz', refreshToken: 'bar', refreshTokenExpiresOn: 'baz', scope: 'foobar' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: 'foobar' }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); }) diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 7bea2d8da..63c48b761 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -65,7 +65,7 @@ describe('AuthorizeHandler', function() { .then(function() { model.saveAuthCode.callCount.should.equal(1); model.saveAuthCode.firstCall.args.should.have.length(3); - model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresOn: 'bar', scope: 'qux' }); + model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresAt: 'bar', scope: 'qux' }); model.saveAuthCode.firstCall.args[1].should.equal('biz'); model.saveAuthCode.firstCall.args[2].should.equal('baz'); }) From 6823cf2ff873490ac65435792cf7bac87ca49069 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Sun, 26 Apr 2015 22:53:18 +0100 Subject: [PATCH 16/39] Rename authCode to authorization code --- .jshintrc | 5 +- Changelog.md => CHANGELOG.md | 0 Readme.md => README.md | 2 +- lib/errors/access-denied-error.js | 4 + lib/errors/invalid-argument-error.js | 1 + lib/errors/invalid-client-error.js | 5 + lib/errors/invalid-grant-error.js | 6 + lib/errors/invalid-request-error.js | 5 + lib/errors/invalid-scope-error.js | 4 + lib/errors/invalid-token-error.js | 4 + lib/errors/server-error.js | 4 + lib/errors/unauthorized-client-error.js | 4 + lib/errors/unsupported-grant-type-error.js | 4 + lib/grant-types/abstract-grant-type.js | 16 +- .../authorization-code-grant-type.js | 70 ++-- lib/grant-types/password-grant-type.js | 8 +- lib/grant-types/refresh-token-grant-type.js | 10 +- lib/handlers/authenticate-handler.js | 59 +-- lib/handlers/authorize-handler.js | 66 ++-- lib/handlers/token-handler.js | 20 +- lib/request.js | 16 +- lib/response-types/code-response-type.js | 4 +- lib/response.js | 14 +- lib/server.js | 8 +- lib/validator/is.js | 12 +- .../authorization-code-grant-type_test.js | 192 +++++----- .../grant-types/password-grant-type_test.js | 56 +-- .../refresh-token-grant-type_test.js | 104 ++++-- .../handlers/authenticate-handler_test.js | 124 +++--- .../handlers/authorize-handler_test.js | 353 +++++++++--------- .../handlers/token-handler_test.js | 130 +++---- test/integration/request_test.js | 42 +-- .../response-types/code-response-type_test.js | 8 +- test/integration/response_test.js | 28 +- test/integration/server_test.js | 12 +- .../authorization-code-grant-type_test.js | 40 +- .../handlers/authenticate-handler_test.js | 14 - test/unit/handlers/authorize-handler_test.js | 39 +- test/unit/server_test.js | 2 +- 39 files changed, 799 insertions(+), 696 deletions(-) rename Changelog.md => CHANGELOG.md (100%) rename Readme.md => README.md (99%) diff --git a/.jshintrc b/.jshintrc index 0cb361ad3..441e9934f 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,14 +1,10 @@ { "bitwise": true, - "browser": true, "curly": true, "eqeqeq": true, "esnext": true, "expr": true, "globalstrict": false, - "globals": { - "Promise": true - }, "immed": true, "indent": 2, "jquery": true, @@ -18,6 +14,7 @@ "noarg": true, "node": true, "noyield": true, + "predef": ["-Promise"], "quotmark": "single", "regexp": true, "smarttabs": true, diff --git a/Changelog.md b/CHANGELOG.md similarity index 100% rename from Changelog.md rename to CHANGELOG.md diff --git a/Readme.md b/README.md similarity index 99% rename from Readme.md rename to README.md index 701f8f2b7..c3db39c3e 100644 --- a/Readme.md +++ b/README.md @@ -1,6 +1,6 @@ # Node OAuth2 Server [![Build Status](https://travis-ci.org/thomseddon/node-oauth2-server.png)](https://travis-ci.org/thomseddon/node-oauth2-server) -Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with [express](http://expressjs.com/) in [node.js](http://nodejs.org/) +Complete, compliant and well tested module for implementing an OAuth2 Server/Provider in [node.js](http://nodejs.org/) ## Installation diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js index 4173f14eb..6b3001f7b 100644 --- a/lib/errors/access-denied-error.js +++ b/lib/errors/access-denied-error.js @@ -9,6 +9,10 @@ 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) { diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index b393f8f8c..d3eb4d434 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -1,3 +1,4 @@ + /** * Module dependencies. */ diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js index 83f04dd0f..621552442 100644 --- a/lib/errors/invalid-client-error.js +++ b/lib/errors/invalid-client-error.js @@ -9,6 +9,11 @@ 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) { diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js index 4fe105b1d..1d7a1071f 100644 --- a/lib/errors/invalid-grant-error.js +++ b/lib/errors/invalid-grant-error.js @@ -9,6 +9,12 @@ 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) { diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js index 4ccc960a4..92d6a5c1d 100644 --- a/lib/errors/invalid-request-error.js +++ b/lib/errors/invalid-request-error.js @@ -9,6 +9,11 @@ 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) { diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index 1a79439af..5606ff29e 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -9,6 +9,10 @@ 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) { diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js index 24a2d548d..876c5b515 100644 --- a/lib/errors/invalid-token-error.js +++ b/lib/errors/invalid-token-error.js @@ -9,6 +9,10 @@ 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) { diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js index ec5ac0860..4d9537f01 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -9,6 +9,10 @@ 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) { diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js index 7026e872a..c1aa3dd6f 100644 --- a/lib/errors/unauthorized-client-error.js +++ b/lib/errors/unauthorized-client-error.js @@ -9,6 +9,10 @@ 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) { diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index ad17877f8..b7d2dea40 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -9,6 +9,10 @@ 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) { diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index 8aee4c310..54492129d 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -32,28 +32,28 @@ function AbstractGrantType(options) { * Generate access token. */ -AbstractGrantType.prototype.generateAccessToken = Promise.method(function() { +AbstractGrantType.prototype.generateAccessToken = function() { if (this.model.generateAccessToken) { - return this.model.generateAccessToken(); + return Promise.try(this.model.generateAccessToken); } return tokenUtil.generateRandomToken(); -}); +}; /** * Generate refresh token. */ -AbstractGrantType.prototype.generateRefreshToken = Promise.method(function() { +AbstractGrantType.prototype.generateRefreshToken = function() { if (this.model.generateRefreshToken) { - return this.model.generateRefreshToken(); + return Promise.try(this.model.generateRefreshToken); } return tokenUtil.generateRandomToken(); -}); +}; /** - * Get access token expires on. + * Get access token expiration date. */ AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { @@ -65,7 +65,7 @@ AbstractGrantType.prototype.getAccessTokenExpiresAt = function() { }; /** - * Get refresh token expires on. + * Get refresh token expiration date. */ AbstractGrantType.prototype.getRefreshTokenExpiresAt = function() { diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 5546fe404..597a0b68e 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -16,19 +16,19 @@ var util = require('util'); * Constructor. */ -function AuthCodeGrantType(options) { +function AuthorizationCodeGrantType(options) { options = options || {}; if (!options.model) { throw new InvalidArgumentError('Missing parameter: `model`'); } - if (!options.model.getAuthCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthCode()`'); + if (!options.model.getAuthorizationCode) { + throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthorizationCode()`'); } - if (!options.model.revokeAuthCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthCode()`'); + if (!options.model.revokeAuthorizationCode) { + throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthorizationCode()`'); } if (!options.model.saveToken) { @@ -42,7 +42,7 @@ function AuthCodeGrantType(options) { * Inherit prototype. */ -util.inherits(AuthCodeGrantType, AbstractGrantType); +util.inherits(AuthorizationCodeGrantType, AbstractGrantType); /** * Handle authorization code grant. @@ -50,7 +50,7 @@ util.inherits(AuthCodeGrantType, AbstractGrantType); * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ -AuthCodeGrantType.prototype.handle = function(request, client) { +AuthorizationCodeGrantType.prototype.handle = function(request, client) { if (!request) { throw new InvalidArgumentError('Missing parameter: `request`'); } @@ -61,13 +61,13 @@ AuthCodeGrantType.prototype.handle = function(request, client) { return Promise.bind(this) .then(function() { - return this.getAuthCode(request, client); + return this.getAuthorizationCode(request, client); }) .tap(function(code) { - return this.revokeAuthCode(code); + return this.revokeAuthorizationCode(code); }) .then(function(code) { - return this.saveToken(code.user, client, code.authCode, code.scope); + return this.saveToken(code.user, client, code.authorizationCode, code.scope); }); }; @@ -75,42 +75,42 @@ AuthCodeGrantType.prototype.handle = function(request, client) { * Get the authorization code. */ -AuthCodeGrantType.prototype.getAuthCode = function(request, client) { +AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, client) { if (!request.body.code) { - return Promise.reject(new InvalidRequestError('Missing parameter: `code`')); + throw new InvalidRequestError('Missing parameter: `code`'); } if (!is.vschar(request.body.code)) { - return Promise.reject(new InvalidRequestError('Invalid parameter: `code`')); + throw new InvalidRequestError('Invalid parameter: `code`'); } - return Promise.try(this.model.getAuthCode, request.body.code) - .then(function(authCode) { - if (!authCode) { + return Promise.try(this.model.getAuthorizationCode, request.body.code) + .then(function(code) { + if (!code) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (!authCode.client) { - throw new ServerError('Server error: `getAuthCode()` did not return a `client` object'); + if (!code.client) { + throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object'); } - if (!authCode.user) { - throw new ServerError('Server error: `getAuthCode()` did not return a `user` object'); + if (!code.user) { + throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object'); } - if (authCode.client.id !== client.id) { + if (code.client.id !== client.id) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (!(authCode.expiresAt instanceof Date)) { + if (!(code.expiresAt instanceof Date)) { throw new ServerError('Server error: `expiresAt` must be a Date instance'); } - if (authCode.expiresAt < new Date()) { + if (code.expiresAt < new Date()) { throw new InvalidGrantError('Invalid grant: authorization code has expired'); } - return authCode; + return code; }); }; @@ -124,36 +124,36 @@ AuthCodeGrantType.prototype.getAuthCode = function(request, client) { * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 */ -AuthCodeGrantType.prototype.revokeAuthCode = Promise.method(function(authCode) { - return Promise.try(this.model.revokeAuthCode, authCode) - .then(function(authCode) { - if (!authCode) { +AuthorizationCodeGrantType.prototype.revokeAuthorizationCode = function(code) { + return Promise.try(this.model.revokeAuthorizationCode, code) + .then(function(code) { + if (!code) { throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } - if (!(authCode.expiresAt instanceof Date)) { + if (!(code.expiresAt instanceof Date)) { throw new ServerError('Server error: `expiresAt` must be a Date instance'); } - if (authCode.expiresAt >= new Date()) { + if (code.expiresAt >= new Date()) { throw new ServerError('Server error: authorization code should be expired'); } - return authCode; + return code; }); -}); +}; /** * Save token. */ -AuthCodeGrantType.prototype.saveToken = function(user, client, authCode, scope) { +AuthorizationCodeGrantType.prototype.saveToken = function(user, client, authorizationCode, scope) { return this.generateAccessToken() .bind(this) .then(function(accessToken) { var token = { accessToken: accessToken, - authCode: authCode, + authorizationCode: authorizationCode, scope: scope }; @@ -165,4 +165,4 @@ AuthCodeGrantType.prototype.saveToken = function(user, client, authCode, scope) * Export constructor. */ -module.exports = AuthCodeGrantType; +module.exports = AuthorizationCodeGrantType; diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index dbcad4b2b..651d42b3a 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -71,19 +71,19 @@ PasswordGrantType.prototype.handle = function(request, client) { PasswordGrantType.prototype.getUser = function(request) { if (!request.body.username) { - return Promise.reject(new InvalidRequestError('Missing parameter: `username`')); + throw new InvalidRequestError('Missing parameter: `username`'); } if (!request.body.password) { - return Promise.reject(new InvalidRequestError('Missing parameter: `password`')); + throw new InvalidRequestError('Missing parameter: `password`'); } if (!is.uchar(request.body.username)) { - return Promise.reject(new InvalidRequestError('Invalid parameter: `username`')); + throw new InvalidRequestError('Invalid parameter: `username`'); } if (!is.uchar(request.body.password)) { - return Promise.reject(new InvalidRequestError('Invalid parameter: `password`')); + throw new InvalidRequestError('Invalid parameter: `password`'); } return Promise.try(this.model.getUser, [request.body.username, request.body.password]) diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 8b6b48217..cab0d186d 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -77,11 +77,11 @@ RefreshTokenGrantType.prototype.handle = function(request, client) { RefreshTokenGrantType.prototype.getRefreshToken = function(request, client) { if (!request.body.refresh_token) { - return Promise.reject(new InvalidRequestError('Missing parameter: `refresh_token`')); + throw new InvalidRequestError('Missing parameter: `refresh_token`'); } if (!is.vschar(request.body.refresh_token)) { - return Promise.reject(new InvalidRequestError('Invalid parameter: `refresh_token`')); + throw new InvalidRequestError('Invalid parameter: `refresh_token`'); } return Promise.try(this.model.getRefreshToken, request.body.refresh_token) @@ -120,7 +120,7 @@ RefreshTokenGrantType.prototype.getRefreshToken = function(request, client) { * @see https://tools.ietf.org/html/rfc6749#section-6 */ -RefreshTokenGrantType.prototype.revokeToken = Promise.method(function(token) { +RefreshTokenGrantType.prototype.revokeToken = function(token) { return Promise.try(this.model.revokeToken, token) .then(function(token) { if (!token) { @@ -132,12 +132,12 @@ RefreshTokenGrantType.prototype.revokeToken = Promise.method(function(token) { } if (token.refreshTokenExpiresAt >= new Date()) { - throw new ServerError('Server error: authorization code should be expired'); + throw new ServerError('Server error: refresh token should be expired'); } return token; }); -}); +}; /** * Save token. diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 1ab0dba24..278275dc5 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -55,6 +55,10 @@ AuthenticateHandler.prototype.handle = function(request) { return this.validateAccessToken(token); }) .tap(function(token) { + if (!this.scope) { + return; + } + return this.validateScope(token); }) .catch(function(e) { @@ -68,9 +72,13 @@ AuthenticateHandler.prototype.handle = function(request) { /** * 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.getToken = Promise.method(function(request) { +AuthenticateHandler.prototype.getToken = function(request) { var headerToken = request.get('Authorization'); var queryToken = request.query.access_token; var bodyToken = request.body.access_token; @@ -92,15 +100,15 @@ AuthenticateHandler.prototype.getToken = Promise.method(function(request) { } throw new InvalidRequestError('Invalid request: no access token given'); -}); +}; /** * Get the token from the request header. * - * (See: http://tools.ietf.org/html/rfc6750#section-2.1) + * @see http://tools.ietf.org/html/rfc6750#section-2.1 */ -AuthenticateHandler.prototype.getTokenFromRequestHeader = Promise.method(function(request) { +AuthenticateHandler.prototype.getTokenFromRequestHeader = function(request) { var token = request.get('Authorization'); var matches = token.match(/Bearer\s(\S+)/); @@ -109,26 +117,37 @@ AuthenticateHandler.prototype.getTokenFromRequestHeader = Promise.method(functio } return matches[1]; -}); +}; /** * Get the token from the request query. * - * (See: http://tools.ietf.org/html/rfc6750#section-2.3) + * "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 = Promise.method(function() { +AuthenticateHandler.prototype.getTokenFromRequestQuery = function() { throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); -}); +}; /** * Get the token from the request body. * - * (See: http://tools.ietf.org/html/rfc6750#section-2.2) + * "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 = Promise.method(function(request) { - if ('GET' === request.method) { +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'); } @@ -137,13 +156,13 @@ AuthenticateHandler.prototype.getTokenFromRequestBody = Promise.method(function( } return request.body.access_token; -}); +}; /** * Get the access token from the model. */ -AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { +AuthenticateHandler.prototype.getAccessToken = function(token) { return Promise.try(this.model.getAccessToken, token) .then(function(accessToken) { if (!accessToken) { @@ -156,13 +175,13 @@ AuthenticateHandler.prototype.getAccessToken = Promise.method(function(token) { return accessToken; }); -}); +}; /** * Validate access token. */ -AuthenticateHandler.prototype.validateAccessToken = Promise.method(function(accessToken) { +AuthenticateHandler.prototype.validateAccessToken = function(accessToken) { if (accessToken.accessTokenExpiresAt && !(accessToken.accessTokenExpiresAt instanceof Date)) { throw new ServerError('Server error: `expires` must be a Date instance'); } @@ -172,17 +191,13 @@ AuthenticateHandler.prototype.validateAccessToken = Promise.method(function(acce } return accessToken; -}); +}; /** * Validate scope. */ -AuthenticateHandler.prototype.validateScope = Promise.method(function(accessToken) { - if (!this.scope) { - return; - } - +AuthenticateHandler.prototype.validateScope = function(accessToken) { return Promise.try(this.model.validateScope, [accessToken, this.scope]).then(function(scope) { if (!scope) { throw new InvalidScopeError('Invalid scope: scope is invalid'); @@ -190,7 +205,7 @@ AuthenticateHandler.prototype.validateScope = Promise.method(function(accessToke return scope; }); -}); +}; /** * Export constructor. diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index dd67ad87e..4022337bb 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -35,8 +35,8 @@ var responseTypes = { function AuthorizeHandler(options) { options = options || {}; - if (!options.authCodeLifetime) { - throw new InvalidArgumentError('Missing parameter: `authCodeLifetime`'); + if (!options.authorizationCodeLifetime) { + throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); } if (!options.model) { @@ -47,11 +47,11 @@ function AuthorizeHandler(options) { throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); } - if (!options.model.saveAuthCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthCode()`'); + if (!options.model.saveAuthorizationCode) { + throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); } - this.authCodeLifetime = options.authCodeLifetime; + this.authorizationCodeLifetime = options.authorizationCodeLifetime; this.authenticateHandler = new AuthenticateHandler(options); this.model = options.model; } @@ -74,8 +74,8 @@ AuthorizeHandler.prototype.handle = function(request, response) { } var fns = [ - this.generateAuthCode(), - this.getAuthCodeLifetime(), + this.generateAuthorizationCode(), + this.getAuthorizationCodeLifetime(), this.getClient(request), this.getScope(request), this.getState(request), @@ -84,8 +84,8 @@ AuthorizeHandler.prototype.handle = function(request, response) { return Promise.all(fns) .bind(this) - .spread(function(authCode, expiresAt, client, scope, state, user) { - return this.saveAuthCode(authCode, expiresAt, scope, client, user) + .spread(function(authorizationCode, expiresAt, client, scope, state, user) { + return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, user) .bind(this) .then(function(code) { var responseType = this.getResponseType(request, code); @@ -110,34 +110,34 @@ AuthorizeHandler.prototype.handle = function(request, response) { }; /** - * Generate auth code. + * Generate authorization code. */ -AuthorizeHandler.prototype.generateAuthCode = Promise.method(function() { - if (this.model.generateAuthCode) { - return this.model.generateAuthCode(); +AuthorizeHandler.prototype.generateAuthorizationCode = function() { + if (this.model.generateAuthorizationCode) { + return Promise.try(this.model.generateAuthorizationCode); } return tokenUtil.generateRandomToken(); -}); +}; /** - * Get auth code lifetime. + * Get authorization code lifetime. */ -AuthorizeHandler.prototype.getAuthCodeLifetime = Promise.method(function() { +AuthorizeHandler.prototype.getAuthorizationCodeLifetime = function() { var expires = new Date(); - expires.setSeconds(expires.getSeconds() + this.authCodeLifetime); + expires.setSeconds(expires.getSeconds() + this.authorizationCodeLifetime); return expires; -}); +}; /** * Get the client from the model. */ -AuthorizeHandler.prototype.getClient = Promise.method(function(request) { +AuthorizeHandler.prototype.getClient = function(request) { var clientId = request.body.client_id || request.query.client_id; if (!clientId) { @@ -178,25 +178,25 @@ AuthorizeHandler.prototype.getClient = Promise.method(function(request) { return client; }); -}); +}; /** * Get scope from the request body. */ -AuthorizeHandler.prototype.getScope = Promise.method(function(request) { +AuthorizeHandler.prototype.getScope = function(request) { if (!is.nqschar(request.body.scope)) { throw new InvalidArgumentError('Invalid parameter: `scope`'); } return request.body.scope; -}); +}; /** * Get state from the request. */ -AuthorizeHandler.prototype.getState = Promise.method(function(request) { +AuthorizeHandler.prototype.getState = function(request) { var state = request.body.state || request.query.state; if (!state) { @@ -208,31 +208,31 @@ AuthorizeHandler.prototype.getState = Promise.method(function(request) { } return state; -}); +}; /** * Get user by calling the authenticate middleware. */ -AuthorizeHandler.prototype.getUser = Promise.method(function(request) { +AuthorizeHandler.prototype.getUser = function(request) { return this.authenticateHandler.handle(request).then(function(token) { return token.user; }); -}); +}; /** - * Save auth code. + * Save authorization code. */ -AuthorizeHandler.prototype.saveAuthCode = Promise.method(function(authCode, expiresAt, scope, client, user) { +AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, user) { var code = { - authCode: authCode, + authorizationCode: authorizationCode, expiresAt: expiresAt, scope: scope }; - return this.model.saveAuthCode(code, client, user); -}); + return Promise.try(this.model.saveAuthorizationCode, [code, client, user]); +}; /** * Get response type. @@ -251,7 +251,7 @@ AuthorizeHandler.prototype.getResponseType = function(request, code) { var Type = responseTypes[responseType]; - return new Type(code.authCode); + return new Type(code.authorizationCode); }; /** @@ -259,7 +259,7 @@ AuthorizeHandler.prototype.getResponseType = function(request, code) { */ AuthorizeHandler.prototype.buildSuccessRedirectUri = function(redirectUri, responseType) { - return responseType.getRedirectUri(redirectUri); + return responseType.buildRedirectUri(redirectUri); }; /** diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 80c5c9928..41fb4fb83 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -72,7 +72,7 @@ TokenHandler.prototype.handle = function(request, response) { throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); } - if ('POST' !== request.method) { + if (request.method !== 'POST') { return Promise.reject(new InvalidRequestError('Invalid request: method must be POST')); } @@ -107,7 +107,7 @@ TokenHandler.prototype.handle = function(request, response) { * Get the client from the model. */ -TokenHandler.prototype.getClient = Promise.method(function(request, response) { +TokenHandler.prototype.getClient = function(request, response) { var credentials = this.getClientCredentials(request); if (!credentials.clientId) { @@ -155,7 +155,7 @@ TokenHandler.prototype.getClient = Promise.method(function(request, response) { throw e; }); -}); +}; /** * Get client credentials. @@ -163,7 +163,7 @@ TokenHandler.prototype.getClient = Promise.method(function(request, response) { * 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) + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 */ TokenHandler.prototype.getClientCredentials = function(request) { @@ -184,7 +184,7 @@ TokenHandler.prototype.getClientCredentials = function(request) { * Handle grant type. */ -TokenHandler.prototype.handleGrantType = Promise.method(function(request, client) { +TokenHandler.prototype.handleGrantType = function(request, client) { var grantType = request.body.grant_type; if (!grantType) { @@ -208,7 +208,7 @@ TokenHandler.prototype.handleGrantType = Promise.method(function(request, client return new Type(options) .handle(request, client); -}); +}; /** * Get token type. @@ -222,25 +222,25 @@ TokenHandler.prototype.getTokenType = function(model) { * Update response when a token is generated. */ -TokenHandler.prototype.updateSuccessResponse = Promise.method(function(response, tokenType) { +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 = Promise.method(function(response, error) { +TokenHandler.prototype.updateErrorResponse = function(response, error) { response.body = { error: error.name, error_description: error.message }; response.status = error.code; -}); +}; /** * Export constructor. diff --git a/lib/request.js b/lib/request.js index 04d4c67c6..1a67689c4 100644 --- a/lib/request.js +++ b/lib/request.js @@ -36,6 +36,14 @@ function Request(options) { } } +/** + * 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. */ @@ -48,14 +56,6 @@ Request.prototype.is = function(types) { return typeis(this, types) || false; }; -/** - * Get a request header. - */ - -Request.prototype.get = function(field) { - return this.headers[field.toLowerCase()]; -}; - /** * Export constructor. */ diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js index 9fae7a9c9..1341c7531 100644 --- a/lib/response-types/code-response-type.js +++ b/lib/response-types/code-response-type.js @@ -19,10 +19,10 @@ function CodeResponseType(code) { } /** - * Get redirect uri. + * Build redirect uri. */ -CodeResponseType.prototype.getRedirectUri = function(redirectUri) { +CodeResponseType.prototype.buildRedirectUri = function(redirectUri) { if (!redirectUri) { throw new InvalidArgumentError('Missing parameter: `redirectUri`'); } diff --git a/lib/response.js b/lib/response.js index 411aaf2bd..52b69fd19 100644 --- a/lib/response.js +++ b/lib/response.js @@ -17,20 +17,20 @@ function Response(options) { } /** - * Redirect response. + * Get a response header. */ -Response.prototype.redirect = function(url) { - this.set('Location', url); - this.status = 302; +Response.prototype.get = function(field) { + return this.headers[field.toLowerCase()]; }; /** - * Get a response header. + * Redirect response. */ -Response.prototype.get = function(field) { - return this.headers[field.toLowerCase()]; +Response.prototype.redirect = function(url) { + this.set('Location', url); + this.status = 302; }; /** diff --git a/lib/server.js b/lib/server.js index cd2bd3fef..1f7421b74 100644 --- a/lib/server.js +++ b/lib/server.js @@ -11,6 +11,10 @@ var TokenHandler = require('./handlers/token-handler'); /** * Constructor. + * + * Default access token lifetime is 1 hour. + * Default authorization code lifetime is 5 minutes. + * Default refresh token lifetime is 2 weeks. */ function OAuth2Server(options) { @@ -22,8 +26,8 @@ function OAuth2Server(options) { this.options = _.assign({ accessTokenLifetime: 60 * 60, - authCodeLifetime: 5 * 60, - refreshTokenLifetime: 1209600 + authorizationCodeLifetime: 5 * 60, + refreshTokenLifetime: 60 * 60 * 24 * 14 }, options); } diff --git a/lib/validator/is.js b/lib/validator/is.js index 942de6874..c2b54ff95 100644 --- a/lib/validator/is.js +++ b/lib/validator/is.js @@ -21,7 +21,7 @@ module.exports = { /** * Validate if a value matches a unicode character. * - * (See: https://tools.ietf.org/html/rfc6749#appendix-A). + * @see https://tools.ietf.org/html/rfc6749#appendix-A */ nchar: function(value) { @@ -31,7 +31,7 @@ module.exports = { /** * Validate if a value matches a unicode character, including exclamation marks. * - * (See: https://tools.ietf.org/html/rfc6749#appendix-A). + * @see https://tools.ietf.org/html/rfc6749#appendix-A */ nqchar: function(value) { @@ -41,7 +41,7 @@ module.exports = { /** * Validate if a value matches a unicode character, including exclamation marks and spaces. * - * (See: https://tools.ietf.org/html/rfc6749#appendix-A). + * @see https://tools.ietf.org/html/rfc6749#appendix-A */ nqschar: function(value) { @@ -52,7 +52,7 @@ module.exports = { * Validate if a value matches a unicode character excluding the carriage * return and linefeed characters. * - * (See: https://tools.ietf.org/html/rfc6749#appendix-A). + * @see https://tools.ietf.org/html/rfc6749#appendix-A */ uchar: function(value) { @@ -62,7 +62,7 @@ module.exports = { /** * Validate if a value matches generic URIs. * - * (See: http://tools.ietf.org/html/rfc3986#section-3). + * @see http://tools.ietf.org/html/rfc3986#section-3 */ uri: function(value) { return rules.URI.test(value); @@ -71,7 +71,7 @@ module.exports = { /** * Validate if a value matches against the printable set of unicode characters. * - * (See: https://tools.ietf.org/html/rfc6749#appendix-A). + * @see https://tools.ietf.org/html/rfc6749#appendix-A */ vschar: function(value) { diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 28a7dde08..112a7ba23 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -29,21 +29,21 @@ describe('AuthorizationCodeGrantType integration', function() { } }); - it('should throw an error if the model does not implement `getAuthCode()`', function() { + 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 `getAuthCode()`'); + e.message.should.equal('Invalid argument: model does not implement `getAuthorizationCode()`'); } }); - it('should throw an error if the model does not implement `revokeAuthCode()`', function() { + it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function() { try { var model = { - getAuthCode: function() {} + getAuthorizationCode: function() {} }; new AuthorizationCodeGrantType({ model: model }); @@ -51,15 +51,15 @@ describe('AuthorizationCodeGrantType integration', function() { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeAuthCode()`'); + 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 = { - getAuthCode: function() {}, - revokeAuthCode: function() {} + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {} }; new AuthorizationCodeGrantType({ model: model }); @@ -75,8 +75,8 @@ describe('AuthorizationCodeGrantType integration', function() { describe('handle()', function() { it('should throw an error if `request` is missing', function() { var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -94,8 +94,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should throw an error if `client` is missing', function() { var client = {}; var model = { - getAuthCode: function() { return { authCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthCode: function() {}, + getAuthorizationCode: function() { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; }, + revokeAuthorizationCode: function() {}, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -105,7 +105,7 @@ describe('AuthorizationCodeGrantType integration', function() { .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthCode()` did not return a `client` object'); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); }); }); @@ -113,8 +113,8 @@ describe('AuthorizationCodeGrantType integration', function() { var client = { id: 'foobar' }; var token = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; }, + getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, + revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: function() { return token; } }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -130,8 +130,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, - revokeAuthCode: function() { return Promise.resolve({ authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }) }, + getAuthorizationCode: function() { return Promise.resolve({ authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }); }, + revokeAuthorizationCode: function() { return Promise.resolve({ authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }) }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -143,8 +143,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support non-promises', function() { var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; }, + getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, + revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -154,54 +154,58 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); - describe('getAuthCode()', function() { + describe('getAuthorizationCode()', function() { it('should throw an error if the request body does not contain `code`', function() { var client = {}; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.getAuthCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `code`'); - }); + 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 = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { code: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return grantType.getAuthCode(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code`'); - }); + 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 `authCode` is missing', function() { + it('should throw an error if `authorizationCode` is missing', function() { var client = {}; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -209,35 +213,35 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); - it('should throw an error if `authCode.client` is missing', function() { + it('should throw an error if `authorizationCode.client` is missing', function() { var client = {}; var model = { - getAuthCode: function() { return { authCode: 12345 }; }, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthCode()` did not return a `client` object'); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); }); }); - it('should throw an error if `authCode.expiresAt` is missing', function() { + it('should throw an error if `authorizationCode.expiresAt` is missing', function() { var client = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: {}, user: {} }; }, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); @@ -245,37 +249,37 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); - it('should throw an error if `authCode.user` is missing', function() { + it('should throw an error if `authorizationCode.user` is missing', function() { var client = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: {}, expiresAt: new Date() }; }, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthCode()` did not return a `user` object'); + 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 = { - getAuthCode: function() { - return { authCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; + getAuthorizationCode: function() { + return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; }, - revokeAuthCode: 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -287,16 +291,16 @@ describe('AuthorizationCodeGrantType integration', function() { var client = { id: 123 }; var date = new Date(new Date() / 2); var model = { - getAuthCode: function() { - return { authCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; + getAuthorizationCode: function() { + return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; }, - revokeAuthCode: 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(InvalidGrantError); @@ -305,91 +309,91 @@ describe('AuthorizationCodeGrantType integration', function() { }); it('should return an auth code', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; + var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return authCode; }, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client) + return grantType.getAuthorizationCode(request, client) .then(function(data) { - data.should.equal(authCode); + data.should.equal(authorizationCode); }) .catch(should.fail); }); it('should support promises', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; + var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return Promise.resolve(authCode); }, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client).should.be.an.instanceOf(Promise); + grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { - var authCode = { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; + var authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; var client = { id: 'foobar' }; var model = { - getAuthCode: function() { return authCode; }, - revokeAuthCode: function() {}, + 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.getAuthCode(request, client).should.be.an.instanceOf(Promise); + grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); }); }); - describe('revokeAuthCode()', function() { + describe('revokeAuthorizationCode()', function() { it('should revoke the auth code', function() { - var authCode = { authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() { return authCode; }, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() { return authorizationCode; }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - return grantType.revokeAuthCode(authCode) + return grantType.revokeAuthorizationCode(authorizationCode) .then(function(data) { - data.should.equal(authCode); + data.should.equal(authorizationCode); }) .catch(should.fail); }); it('should support promises', function() { - var authCode = { authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() { return Promise.resolve(authCode); }, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() { return Promise.resolve(authorizationCode); }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthCode(authCode).should.be.an.instanceOf(Promise); + grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { - var authCode = { authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; + var authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() { return authCode; }, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() { return authorizationCode; }, saveToken: function() {} }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthCode(authCode).should.be.an.instanceOf(Promise); + grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); }); @@ -397,8 +401,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should save the token', function() { var token = {}; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: function() { return token; } }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -413,8 +417,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support promises', function() { var token = {}; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: function() { return Promise.resolve(token); } }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); @@ -425,8 +429,8 @@ describe('AuthorizationCodeGrantType integration', function() { it('should support non-promises', function() { var token = {}; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: function() { return token; } }; var grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index f4f183b4d..2de97672d 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -143,12 +143,14 @@ describe('PasswordGrantType integration', function() { var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `username`'); - }); + 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() { @@ -159,12 +161,14 @@ describe('PasswordGrantType integration', function() { var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `password`'); - }); + 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() { @@ -175,12 +179,14 @@ describe('PasswordGrantType integration', function() { var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'ø倣‰', password: 'foobar' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `username`'); - }); + 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() { @@ -191,12 +197,14 @@ describe('PasswordGrantType integration', function() { var grantType = new PasswordGrantType({ accessTokenLifetime: 123, model: model }); var request = new Request({ body: { username: 'foobar', password: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return grantType.getUser(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `password`'); - }); + 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() { diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 06ca0e8c8..f152b72e0 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -166,12 +166,14 @@ describe('RefreshTokenGrantType integration', function() { var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `refresh_token`'); - }); + 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 the requested `refreshToken` is invalid', function() { @@ -184,12 +186,14 @@ describe('RefreshTokenGrantType integration', function() { var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: { refresh_token: [] }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `refresh_token`'); - }); + 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 `refreshToken` is missing', function() { @@ -280,12 +284,14 @@ describe('RefreshTokenGrantType integration', function() { var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `refresh_token`'); - }); + 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 `refresh_token` is invalid', function() { @@ -300,12 +306,14 @@ describe('RefreshTokenGrantType integration', function() { var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); var request = new Request({ body: { refresh_token: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return grantType.getRefreshToken(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `refresh_token`'); - }); + 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() { @@ -397,6 +405,54 @@ describe('RefreshTokenGrantType integration', function() { }); 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 throw an error if the `token.refreshTokenExpiresAt` is invalid', function() { + var model = { + getRefreshToken: function() {}, + revokeToken: function() { return { refreshTokenExpiresAt: [] }; }, + saveToken: function() {} + }; + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + + grantType.revokeToken({}) + .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 throw an error if the `token.refreshTokenExpiresAt` is not expired', function() { + var model = { + getRefreshToken: function() {}, + revokeToken: function() { return { refreshTokenExpiresAt: new Date(new Date() * 2) }; }, + saveToken: function() {} + }; + var grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); + + grantType.revokeToken({}) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: refresh token should be expired'); + }); + }); + it('should revoke the token', function() { var token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; var model = { diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 6d477b883..f7a81f871 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -155,24 +155,28 @@ describe('AuthenticateHandler integration', function() { query: { access_token: 'foo' } }); - return handler.getToken(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: only one authentication method is allowed'); - }); + try { + handler.getToken(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: {} }); - return handler.getToken(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: no access token given'); - }); + try { + handler.getToken(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: no access token given'); + } }); }); @@ -188,12 +192,14 @@ describe('AuthenticateHandler integration', function() { query: {} }); - return handler.getTokenFromRequestHeader(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: malformed authorization header'); - }); + 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() { @@ -207,11 +213,9 @@ describe('AuthenticateHandler integration', function() { query: {} }); - return handler.getTokenFromRequestHeader(request) - .then(function(bearerToken) { - bearerToken.should.equal('foo'); - }) - .catch(should.fail); + var bearerToken = handler.getTokenFromRequestHeader(request); + + bearerToken.should.equal('foo'); }); }); @@ -219,12 +223,14 @@ describe('AuthenticateHandler integration', function() { it('should throw an error if the query contains a token', function() { var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - return handler.getTokenFromRequestQuery() - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: do not send bearer tokens in query URLs'); - }); + 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'); + } }); }); @@ -238,12 +244,14 @@ describe('AuthenticateHandler integration', function() { query: {} }); - return handler.getTokenFromRequestBody(request) - .then(should.fail) - .catch(function(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'); - }); + 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() { @@ -255,12 +263,14 @@ describe('AuthenticateHandler integration', function() { query: {} }); - return handler.getTokenFromRequestBody(request) - .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'); - }); + 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() { @@ -351,35 +361,21 @@ describe('AuthenticateHandler integration', function() { var accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - return handler.validateAccessToken(accessToken) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token has expired'); - }); + 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: {} }; var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - return handler.validateAccessToken(accessToken) - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - handler.validateAccessToken('foo').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - handler.validateAccessToken('foo').should.be.an.instanceOf(Promise); + handler.validateAccessToken(accessToken).should.equal(accessToken); }); }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 87d1c9f42..aeb64e751 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -13,7 +13,6 @@ var InvalidRequestError = require('../../../lib/errors/invalid-request-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'); @@ -24,20 +23,20 @@ var url = require('url'); describe('AuthorizeHandler integration', function() { describe('constructor()', function() { - it('should throw an error if `options.authCodeLifetime` is missing', function() { + it('should throw an error if `options.authorizationCodeLifetime` is missing', function() { try { new AuthorizeHandler(); should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `authCodeLifetime`'); + e.message.should.equal('Missing parameter: `authorizationCodeLifetime`'); } }); it('should throw an error if `options.model` is missing', function() { try { - new AuthorizeHandler({ authCodeLifetime: 120 }); + new AuthorizeHandler({ authorizationCodeLifetime: 120 }); should.fail(); } catch (e) { @@ -48,7 +47,7 @@ describe('AuthorizeHandler integration', function() { it('should throw an error if the model does not implement `getClient()`', function() { try { - new AuthorizeHandler({ authCodeLifetime: 120, model: {} }); + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); should.fail(); } catch (e) { @@ -57,25 +56,25 @@ describe('AuthorizeHandler integration', function() { } }); - it('should throw an error if the model does not implement `saveAuthCode()`', function() { + it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { try { - new AuthorizeHandler({ authCodeLifetime: 120, model: { getClient: function() {} } }); + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: function() {} } }); should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveAuthCode()`'); + e.message.should.equal('Invalid argument: model does not implement `saveAuthorizationCode()`'); } }); it('should throw an error if the model does not implement `getAccessToken()`', function() { var model = { getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; try { - new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); should.fail(); } catch (e) { @@ -84,24 +83,24 @@ describe('AuthorizeHandler integration', function() { } }); - it('should set the `authCodeLifetime`', function() { + it('should set the `authorizationCodeLifetime`', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - handler.authCodeLifetime.should.equal(120); + handler.authorizationCodeLifetime.should.equal(120); }); it('should set the `authenticateHandler`', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); }); @@ -110,9 +109,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); handler.model.should.equal(model); }); @@ -123,9 +122,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); try { handler.handle(); @@ -141,9 +140,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -160,9 +159,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: { allowed: 'false' } }); var response = new Response({ body: {}, headers: {} }); @@ -182,11 +181,11 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, - saveAuthCode: function() { + saveAuthorizationCode: function() { throw new Error('Unhandled exception'); } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345, @@ -217,11 +216,11 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, - saveAuthCode: function() { + saveAuthorizationCode: function() { throw new AccessDeniedError('Cannot request this auth code'); } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345, @@ -253,11 +252,11 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return client; }, - saveAuthCode: function() { - return { authCode: 12345, client: client }; + saveAuthorizationCode: function() { + return { authorizationCode: 12345, client: client }; } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345, @@ -289,11 +288,11 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return client; }, - saveAuthCode: function() { - return { authCode: 12345, client: client }; + saveAuthorizationCode: function() { + return { authorizationCode: 12345, client: client }; } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345, @@ -312,7 +311,7 @@ describe('AuthorizeHandler integration', function() { return handler.handle(request, response) .then(function(data) { data.should.eql({ - authCode: 12345, + authorizationCode: 12345, client: client }); }) @@ -320,16 +319,16 @@ describe('AuthorizeHandler integration', function() { }); }); - describe('generateAuthCode()', function() { + describe('generateAuthorizationCode()', function() { it('should return an auth code', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.generateAuthCode() + return handler.generateAuthorizationCode() .then(function(data) { data.should.be.a.sha1; }) @@ -338,47 +337,43 @@ describe('AuthorizeHandler integration', function() { it('should support promises', function() { var model = { - generateAuthCode: function() { + generateAuthorizationCode: function() { return Promise.resolve({}); }, getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - handler.generateAuthCode().should.be.an.instanceOf(Promise); + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); it('should support non-promises', function() { var model = { - generateAuthCode: function() { + generateAuthorizationCode: function() { return {}; }, getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - handler.generateAuthCode().should.be.an.instanceOf(Promise); + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); }); }); - describe('getAuthCodeLifetime()', function() { + describe('getAuthorizationCodeLifetime()', function() { it('should return a date', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.getAuthCodeLifetime() - .then(function(data) { - data.should.be.an.instanceOf(Date); - }) - .catch(should.fail); + handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); }); }); @@ -387,60 +382,66 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `client_id`'); - }); + 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() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 'ø倣‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - }); + 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() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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: {} }); - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - }); + 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() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -455,9 +456,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -474,9 +475,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return {}; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -493,9 +494,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: [] }; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -510,9 +511,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() { return { grants: ['authorization_code'] }; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -529,9 +530,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: ['authorization_code'], redirectUri: 'https://example.com' }; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -548,9 +549,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return Promise.resolve({ grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }); }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345 }, headers: {}, @@ -567,9 +568,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { client_id: 12345 }, headers: {}, @@ -588,9 +589,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return client; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -609,9 +610,9 @@ describe('AuthorizeHandler integration', function() { getClient: function() { return client; }, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -628,33 +629,31 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { scope: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `scope`'); - }); + try { + handler.getScope(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `scope`'); + } }); it('should return the scope', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - return handler.getScope(request) - .then(function(scope) { - scope.should.equal('foo'); - }) - .catch(should.fail); + handler.getScope(request).should.equal('foo'); }); }); @@ -663,34 +662,38 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return handler.getState(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `state`'); - }); + 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() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'ø倣‰' } }); - return handler.getState(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `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() { @@ -698,16 +701,12 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); - return handler.getState(request) - .then(function(data) { - data.should.equal('foobar'); - }) - .catch(should.fail); + handler.getState(request).should.equal('foobar'); }); }); @@ -716,16 +715,12 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); - return handler.getState(request) - .then(function(data) { - data.should.equal('foobar'); - }) - .catch(should.fail); + handler.getState(request).should.equal('foobar'); }); }); }); @@ -738,9 +733,9 @@ describe('AuthorizeHandler integration', function() { return { user: user }; }, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); return handler.getUser(request) @@ -751,49 +746,49 @@ describe('AuthorizeHandler integration', function() { }); }); - describe('saveAuthCode()', function() { + describe('saveAuthorizationCode()', function() { it('should return an auth code', function() { - var authCode = {}; + var authorizationCode = {}; var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() { - return authCode; + saveAuthorizationCode: function() { + return authorizationCode; } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.saveAuthCode('foo', 'bar', 'biz', 'baz') + return handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz') .then(function(data) { - data.should.equal(authCode); + data.should.equal(authorizationCode); }) .catch(should.fail); }); - it('should support promises when calling `model.saveAuthCode()`', function() { + it('should support promises when calling `model.saveAuthorizationCode()`', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() { + saveAuthorizationCode: function() { return Promise.resolve({}); } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - handler.saveAuthCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); + handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); - it('should support non-promises when calling `model.saveAuthCode()`', function() { + it('should support non-promises when calling `model.saveAuthorizationCode()`', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() { + saveAuthorizationCode: function() { return {}; } }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - handler.saveAuthCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); + handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); }); @@ -802,9 +797,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { @@ -821,9 +816,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); try { @@ -841,11 +836,11 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - var responseType = handler.getResponseType(request, { authCode: 123 }); + var responseType = handler.getResponseType(request, { authorizationCode: 123 }); responseType.should.be.an.instanceOf(CodeResponseType); }); @@ -856,11 +851,11 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); - var responseType = handler.getResponseType(request, { authCode: 123 }); + var responseType = handler.getResponseType(request, { authorizationCode: 123 }); responseType.should.be.an.instanceOf(CodeResponseType); }); @@ -872,9 +867,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var responseType = new CodeResponseType(12345); var redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); @@ -888,9 +883,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); @@ -901,9 +896,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client'); @@ -915,9 +910,9 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var response = new Response({ body: {}, headers: {} }); var uri = url.parse('http://example.com/cb'); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index e71ccbb0a..e248d374c 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -303,12 +303,14 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: { client_id: 'ø倣‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - }); + 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() { @@ -319,12 +321,14 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: { client_id: 'foo', client_secret: 'ø倣‰' }, headers: {}, method: {}, query: {} }); - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_secret`'); - }); + 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() { @@ -521,12 +525,14 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - return handler.handleGrantType(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `grant_type`'); - }); + 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() { @@ -537,12 +543,14 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: { grant_type: '~foo~' }, headers: {}, method: {}, query: {} }); - return handler.handleGrantType(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `grant_type`'); - }); + 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() { @@ -553,12 +561,14 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: { grant_type: 'foobar' }, headers: {}, method: {}, query: {} }); - return handler.handleGrantType(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(UnsupportedGrantTypeError); - e.message.should.equal('Unsupported grant type: `grant_type` is invalid'); - }); + 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() { @@ -570,12 +580,14 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, query: {} }); - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - }); + 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() { @@ -601,10 +613,10 @@ describe('TokenHandler integration', function() { var client = { id: 'foobar', grants: ['authorization_code'] }; var token = {}; var model = { - getAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, + getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, getClient: function() {}, saveToken: function() { return token; }, - revokeAuthCode: function() { return { authCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; } + 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({ @@ -756,11 +768,9 @@ describe('TokenHandler integration', function() { var tokenType = new BearerTokenType('foo', 'bar', 'biz'); var response = new Response({ body: {}, headers: {} }); - return handler.updateSuccessResponse(response, tokenType) - .then(function() { - response.body.should.eql({ access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', token_type: 'bearer' }); - }) - .catch(should.fail); + 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() { @@ -772,11 +782,9 @@ describe('TokenHandler integration', function() { var tokenType = new BearerTokenType('foo', 'bar', 'biz'); var response = new Response({ body: {}, headers: {} }); - return handler.updateSuccessResponse(response, tokenType) - .then(function() { - response.get('Cache-Control').should.equal('no-store'); - }) - .catch(should.fail); + handler.updateSuccessResponse(response, tokenType); + + response.get('Cache-Control').should.equal('no-store'); }); it('should set the `Pragma` header', function() { @@ -788,11 +796,9 @@ describe('TokenHandler integration', function() { var tokenType = new BearerTokenType('foo', 'bar', 'biz'); var response = new Response({ body: {}, headers: {} }); - return handler.updateSuccessResponse(response, tokenType) - .then(function() { - response.get('Pragma').should.equal('no-cache'); - }) - .catch(should.fail); + handler.updateSuccessResponse(response, tokenType); + + response.get('Pragma').should.equal('no-cache'); }); }); @@ -806,12 +812,10 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var response = new Response({ body: {}, headers: {} }); - return handler.updateErrorResponse(response, error) - .then(function() { - response.body.error.should.equal('access_denied'); - response.body.error_description.should.equal('Cannot request a token'); - }) - .catch(should.fail); + 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() { @@ -823,11 +827,9 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var response = new Response({ body: {}, headers: {} }); - return handler.updateErrorResponse(response, error) - .then(function() { - response.status.should.equal(400); - }) - .catch(should.fail); + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); }); }); }); diff --git a/test/integration/request_test.js b/test/integration/request_test.js index 63f6cd919..54cc7a6d5 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -71,6 +71,27 @@ describe('Request integration', function() { }); }); + 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({ @@ -134,25 +155,4 @@ describe('Request integration', function() { request.is('text/html').should.be.false; }); }); - - 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'); - }); - }); }); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index 844b9c8ed..9e6d6571f 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -32,12 +32,12 @@ describe('CodeResponseType integration', function() { }); }); - describe('getRedirectUri()', function() { + describe('buildRedirectUri()', function() { it('should throw an error if the `redirectUri` is missing', function() { var responseType = new CodeResponseType('foo'); try { - responseType.getRedirectUri(); + responseType.buildRedirectUri(); should.fail(); } catch (e) { @@ -48,14 +48,14 @@ describe('CodeResponseType integration', function() { it('should return the new redirect uri and set the `code` and `state` in the query', function() { var responseType = new CodeResponseType('foo'); - var redirectUri = responseType.getRedirectUri('http://example.com/cb'); + var redirectUri = responseType.buildRedirectUri('http://example.com/cb'); url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); }); it('should return the new redirect uri and append the `code` and `state` in the query', function() { var responseType = new CodeResponseType('foo'); - var redirectUri = responseType.getRedirectUri('http://example.com/cb?foo=bar'); + var redirectUri = responseType.buildRedirectUri('http://example.com/cb?foo=bar'); url.format(redirectUri).should.equal('http://example.com/cb?foo=bar&code=foo'); }); diff --git a/test/integration/response_test.js b/test/integration/response_test.js index 774c6cfc4..5b2c81ffe 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -30,6 +30,20 @@ describe('Response integration', function() { }); }); + 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: {} }); @@ -48,20 +62,6 @@ describe('Response integration', function() { }); }); - 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('set()', function() { it('should set the `field`', function() { var response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 5dfd56a22..6b8ecb325 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -40,10 +40,10 @@ describe('Server integration', function() { server.options.accessTokenLifetime.should.equal(3600); }); - it('should set the default `authCodeLifetime`', function() { + it('should set the default `authorizationCodeLifetime`', function() { var server = new Server({ model: {} }); - server.options.authCodeLifetime.should.equal(300); + server.options.authorizationCodeLifetime.should.equal(300); }); it('should set the default `refreshTokenLifetime`', function() { @@ -89,8 +89,8 @@ describe('Server integration', function() { getClient: function() { return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, - saveAuthCode: function() { - return { authCode: 123 }; + saveAuthorizationCode: function() { + return { authorizationCode: 123 }; } }; var server = new Server({ model: model }); @@ -109,8 +109,8 @@ describe('Server integration', function() { getClient: function() { return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; }, - saveAuthCode: function() { - return { authCode: 123 }; + saveAuthorizationCode: function() { + return { authorizationCode: 123 }; } }; var server = new Server({ model: model }); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index 5cdfa7bb5..d2dee9822 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -14,42 +14,42 @@ var should = require('should'); */ describe('AuthorizationCodeGrantType', function() { - describe('getAuthCode()', function() { - it('should call `model.getAuthCode()`', function() { + describe('getAuthorizationCode()', function() { + it('should call `model.getAuthorizationCode()`', function() { var model = { - getAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), user: {} }), - revokeAuthCode: function() {}, + 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.getAuthCode(request, client) + return handler.getAuthorizationCode(request, client) .then(function() { - model.getAuthCode.callCount.should.equal(1); - model.getAuthCode.firstCall.args.should.have.length(1); - model.getAuthCode.firstCall.args[0].should.equal(12345); + model.getAuthorizationCode.callCount.should.equal(1); + model.getAuthorizationCode.firstCall.args.should.have.length(1); + model.getAuthorizationCode.firstCall.args[0].should.equal(12345); }) .catch(should.fail); }); }); - describe('revokeAuthCode()', function() { - it('should call `model.revokeAuthCode()`', function() { + describe('revokeAuthorizationCode()', function() { + it('should call `model.revokeAuthorizationCode()`', function() { var model = { - getAuthCode: function() {}, - revokeAuthCode: sinon.stub().returns({ authCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }), + getAuthorizationCode: function() {}, + revokeAuthorizationCode: sinon.stub().returns({ authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }), saveToken: function() {} }; var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - var authCode = {}; + var authorizationCode = {}; - return handler.revokeAuthCode(authCode) + return handler.revokeAuthorizationCode(authorizationCode) .then(function() { - model.revokeAuthCode.callCount.should.equal(1); - model.revokeAuthCode.firstCall.args.should.have.length(1); - model.revokeAuthCode.firstCall.args[0].should.equal(authCode); + model.revokeAuthorizationCode.callCount.should.equal(1); + model.revokeAuthorizationCode.firstCall.args.should.have.length(1); + model.revokeAuthorizationCode.firstCall.args[0].should.equal(authorizationCode); }) .catch(should.fail); }); @@ -60,8 +60,8 @@ describe('AuthorizationCodeGrantType', function() { var client = {}; var user = {}; var model = { - getAuthCode: function() {}, - revokeAuthCode: function() {}, + getAuthorizationCode: function() {}, + revokeAuthorizationCode: function() {}, saveToken: sinon.stub().returns(true) }; var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); @@ -72,7 +72,7 @@ describe('AuthorizationCodeGrantType', function() { .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', authCode: 'foobar', scope: 'foobiz' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', authorizationCode: 'foobar', scope: 'foobiz' }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); }) diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index a94ddc0ae..41e1fd49e 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -93,20 +93,6 @@ describe('AuthenticateHandler', function() { }); describe('validateScope()', function() { - it('should not call `validateScope` if scope is not defined', function() { - var model = { - getAccessToken: function() {}, - validateScope: sinon.stub() - }; - var handler = new AuthenticateHandler({ model: model }); - - return handler.validateScope('foo') - .then(function() { - model.validateScope.callCount.should.equal(0); - }) - .catch(should.fail); - }); - it('should call `model.getAccessToken()` if scope is defined', function() { var model = { getAccessToken: function() {}, diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 63c48b761..3e55f4734 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -13,20 +13,19 @@ var should = require('should'); */ describe('AuthorizeHandler', function() { - describe('generateAuthCode()', function() { - it('should call `model.generateAuthCode()`', function() { + describe('generateAuthorizationCode()', function() { + it('should call `model.generateAuthorizationCode()`', function() { var model = { - generateAuthCode: sinon.stub().returns({}), + generateAuthorizationCode: sinon.stub().returns({}), getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.generateAuthCode() + return handler.generateAuthorizationCode() .then(function() { - model.generateAuthCode.callCount.should.equal(1); - model.generateAuthCode.firstCall.args.should.have.length(0); + model.generateAuthorizationCode.callCount.should.equal(1); }) .catch(should.fail); }); @@ -37,9 +36,9 @@ describe('AuthorizeHandler', function() { var model = { getAccessToken: function() {}, getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }), - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + 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) @@ -52,22 +51,22 @@ describe('AuthorizeHandler', function() { }); }); - describe('saveAuthCode()', function() { - it('should call `model.saveAuthCode()`', function() { + describe('saveAuthorizationCode()', function() { + it('should call `model.saveAuthorizationCode()`', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: sinon.stub().returns({}) + saveAuthorizationCode: sinon.stub().returns({}) }; - var handler = new AuthorizeHandler({ authCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.saveAuthCode('foo', 'bar', 'qux', 'biz', 'baz') + return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz') .then(function() { - model.saveAuthCode.callCount.should.equal(1); - model.saveAuthCode.firstCall.args.should.have.length(3); - model.saveAuthCode.firstCall.args[0].should.eql({ authCode: 'foo', expiresAt: 'bar', scope: 'qux' }); - model.saveAuthCode.firstCall.args[1].should.equal('biz'); - model.saveAuthCode.firstCall.args[2].should.equal('baz'); + 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', scope: 'qux' }); + model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('baz'); }) .catch(should.fail); }); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index 2e087c3ee..f65fce98f 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -37,7 +37,7 @@ describe('Server', function() { var model = { getAccessToken: function() {}, getClient: function() {}, - saveAuthCode: function() {} + saveAuthorizationCode: function() {} }; var server = new Server({ model: model }); From 54f5e9ec5536f5bd07063ae5f3c63cad2c413ea9 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 4 May 2015 18:15:30 +0100 Subject: [PATCH 17/39] Improve error handling in authenticate handler --- lib/errors/invalid-argument-error.js | 6 +-- lib/errors/unauthorized-request-error.js | 40 +++++++++++++++++++ lib/handlers/authenticate-handler.js | 3 +- .../handlers/authenticate-handler_test.js | 5 ++- 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 lib/errors/unauthorized-request-error.js diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index d3eb4d434..a3c95abe4 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -4,7 +4,7 @@ */ var _ = require('lodash'); -var OAuthError = require('standard-error'); +var StandardError = require('standard-error'); var util = require('util'); /** @@ -17,14 +17,14 @@ function InvalidArgumentError(message, properties) { name: 'invalid_argument' }, properties); - OAuthError.call(this, message, properties); + StandardError.call(this, message, properties); } /** * Inherit prototype. */ -util.inherits(InvalidArgumentError, OAuthError); +util.inherits(InvalidArgumentError, StandardError); /** * Export constructor. diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js new file mode 100644 index 000000000..1e26ce403 --- /dev/null +++ b/lib/errors/unauthorized-request-error.js @@ -0,0 +1,40 @@ + +/** + * 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/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 278275dc5..b4f615714 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -11,6 +11,7 @@ var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); var Request = require('../request'); var ServerError = require('../errors/server-error'); +var UnauthorizedRequestError = require('../errors/unauthorized-request-error'); /** * Constructor. @@ -99,7 +100,7 @@ AuthenticateHandler.prototype.getToken = function(request) { return this.getTokenFromRequestBody(request); } - throw new InvalidRequestError('Invalid request: no access token given'); + throw new UnauthorizedRequestError('Unauthorized request: no authentication given'); }; /** diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index f7a81f871..bf6478929 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -12,6 +12,7 @@ var InvalidTokenError = require('../../../lib/errors/invalid-token-error'); var Promise = require('bluebird'); var Request = require('../../../lib/request'); var ServerError = require('../../../lib/errors/server-error'); +var UnauthorizedRequestError = require('../../../lib/errors/unauthorized-request-error'); var should = require('should'); /** @@ -174,8 +175,8 @@ describe('AuthenticateHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: no access token given'); + e.should.be.an.instanceOf(UnauthorizedRequestError); + e.message.should.equal('Unauthorized request: no authentication given'); } }); }); From 8bd115a63705564b8302dfb4a24c43d5b92cf832 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 18 May 2015 11:02:45 +0100 Subject: [PATCH 18/39] Improve default options --- lib/errors/invalid-scope-error.js | 1 - lib/errors/unsupported-grant-type-error.js | 2 +- lib/server.js | 19 ++--- test/integration/server_test.js | 87 +++++++++++++++++----- 4 files changed, 78 insertions(+), 31 deletions(-) diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index 5606ff29e..52859e44a 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -22,7 +22,6 @@ function InvalidScopeError(message, properties) { }, properties); OAuthError.call(this, message, properties); - } /** diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index b7d2dea40..ad171c430 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -22,7 +22,7 @@ function UnsupportedGrantTypeError(message, properties) { }, properties); OAuthError.call(this, message, properties); - } +} /** * Inherit prototype. diff --git a/lib/server.js b/lib/server.js index 1f7421b74..654e3e08c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -11,10 +11,6 @@ var TokenHandler = require('./handlers/token-handler'); /** * Constructor. - * - * Default access token lifetime is 1 hour. - * Default authorization code lifetime is 5 minutes. - * Default refresh token lifetime is 2 weeks. */ function OAuth2Server(options) { @@ -24,11 +20,7 @@ function OAuth2Server(options) { throw new InvalidArgumentError('Missing parameter: `model`'); } - this.options = _.assign({ - accessTokenLifetime: 60 * 60, - authorizationCodeLifetime: 5 * 60, - refreshTokenLifetime: 60 * 60 * 24 * 14 - }, options); + this.options = options; } /** @@ -48,7 +40,9 @@ OAuth2Server.prototype.authenticate = function(request, options, callback) { */ OAuth2Server.prototype.authorize = function(request, response, options, callback) { - options = _.assign({}, this.options, options); + options = _.assign({ + authorizationCodeLifetime: 5 * 60 // 5 minutes. + }, this.options, options); return new AuthorizeHandler(options) .handle(request, response) @@ -60,7 +54,10 @@ OAuth2Server.prototype.authorize = function(request, response, options, callback */ OAuth2Server.prototype.token = function(request, response, options, callback) { - options = _.assign({}, this.options, options); + options = _.assign({ + accessTokenLifetime: 60 * 60, // 1 hour. + refreshTokenLifetime: 60 * 60 * 24 * 14 // 2 weeks. + }, this.options, options); return new TokenHandler(options) .handle(request, response) diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 6b8ecb325..d9e76a468 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -33,24 +33,6 @@ describe('Server integration', function() { server.options.model.should.equal(model); }); - - it('should set the default `accessTokenLifetime`', function() { - var server = new Server({ model: {} }); - - server.options.accessTokenLifetime.should.equal(3600); - }); - - it('should set the default `authorizationCodeLifetime`', function() { - var server = new Server({ model: {} }); - - server.options.authorizationCodeLifetime.should.equal(300); - }); - - it('should set the default `refreshTokenLifetime`', function() { - var server = new Server({ model: {} }); - - server.options.refreshTokenLifetime.should.equal(1209600); - }); }); describe('authenticate()', function() { @@ -81,6 +63,29 @@ describe('Server integration', function() { }); describe('authorize()', function() { + it('should set the default `authorizationCodeLifetime`', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + }, + getClient: function() { + return { grants: ['authorization_code'], redirectUri: '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.authorizationCodeLifetime.should.equal(300); + }) + .catch(should.fail); + }); + it('should return a promise', function() { var model = { getAccessToken: function() { @@ -122,6 +127,52 @@ describe('Server integration', function() { }); describe('token()', function() { + it('should set the default `accessTokenLifetime`', 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: {} }); + + return server.token(request, response) + .then(function() { + this.accessTokenLifetime.should.equal(3600); + }) + .catch(should.fail); + }); + + it('should set the default `refreshTokenLifetime`', 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: {} }); + + return server.token(request, response) + .then(function() { + this.refreshTokenLifetime.should.equal(1209600); + }) + .catch(should.fail); + }); + it('should return a promise', function() { var model = { getClient: function() { From a1013b3cafa5bb6e0680a22e619e8e5de7449042 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 18 May 2015 12:16:10 +0100 Subject: [PATCH 19/39] Add X-OAuth scope headers --- lib/handlers/authenticate-handler.js | 34 +++++++- lib/handlers/authorize-handler.js | 6 +- lib/server.js | 9 ++- .../handlers/authenticate-handler_test.js | 77 ++++++++++++++++--- .../handlers/authorize-handler_test.js | 3 +- test/integration/server_test.js | 50 ++++++------ .../handlers/authenticate-handler_test.js | 2 +- 7 files changed, 137 insertions(+), 44 deletions(-) diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index b4f615714..0f71c42ca 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -10,6 +10,7 @@ var InvalidTokenError = require('../errors/invalid-token-error'); var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); var Request = require('../request'); +var Response = require('../response'); var ServerError = require('../errors/server-error'); var UnauthorizedRequestError = require('../errors/unauthorized-request-error'); @@ -28,10 +29,20 @@ function AuthenticateHandler(options) { 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.validateScope) { throw new InvalidArgumentError('Invalid argument: model does not implement `validateScope()`'); } + this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; + this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; this.model = options.model; this.scope = options.scope; } @@ -40,11 +51,15 @@ function AuthenticateHandler(options) { * Authenticate Handler. */ -AuthenticateHandler.prototype.handle = function(request) { +AuthenticateHandler.prototype.handle = function(request, response) { if (!(request instanceof Request)) { throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); } + if (!(response instanceof Response)) { + throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); + } + return Promise.bind(this) .then(function() { return this.getToken(request); @@ -62,6 +77,9 @@ AuthenticateHandler.prototype.handle = function(request) { return this.validateScope(token); }) + .tap(function(token) { + return this.updateResponse(response, token); + }) .catch(function(e) { if (!(e instanceof OAuthError)) { throw new ServerError(e); @@ -208,6 +226,20 @@ AuthenticateHandler.prototype.validateScope = function(accessToken) { }); }; +/** + * Update response. + */ + +AuthenticateHandler.prototype.updateResponse = function(response, accessToken) { + if (this.addAcceptedScopesHeader) { + response.set('X-Accepted-OAuth-Scopes', this.scope); + } + + if (this.addAuthorizedScopesHeader) { + response.set('X-OAuth-Scopes', accessToken.scope); + } +}; + /** * Export constructor. */ diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 4022337bb..e08ba2ea5 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -79,7 +79,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { this.getClient(request), this.getScope(request), this.getState(request), - this.getUser(request) + this.getUser(request, response) ]; return Promise.all(fns) @@ -214,8 +214,8 @@ AuthorizeHandler.prototype.getState = function(request) { * Get user by calling the authenticate middleware. */ -AuthorizeHandler.prototype.getUser = function(request) { - return this.authenticateHandler.handle(request).then(function(token) { +AuthorizeHandler.prototype.getUser = function(request, response) { + return this.authenticateHandler.handle(request, response).then(function(token) { return token.user; }); }; diff --git a/lib/server.js b/lib/server.js index 654e3e08c..045be77ef 100644 --- a/lib/server.js +++ b/lib/server.js @@ -27,11 +27,14 @@ function OAuth2Server(options) { * Authenticate a token. */ -OAuth2Server.prototype.authenticate = function(request, options, callback) { - options = _.assign({}, this.options, options); +OAuth2Server.prototype.authenticate = function(request, response, options, callback) { + options = _.assign({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true + }, this.options, options); return new AuthenticateHandler(options) - .handle(request) + .handle(request, response) .nodeify(callback); }; diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index bf6478929..cd6fd5c34 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -11,6 +11,7 @@ var InvalidScopeError = require('../../../lib/errors/invalid-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'); @@ -43,10 +44,32 @@ describe('AuthenticateHandler integration', function() { } }); - it('should throw an error if `scope` was given and the model does not implement `validateScope()`', function() { + 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 `validateScope()`', function() { + try { + new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: { getAccessToken: function() {} }, scope: 'foobar' }); + should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); @@ -66,7 +89,12 @@ describe('AuthenticateHandler integration', function() { getAccessToken: function() {}, validateScope: function() {} }; - var grantType = new AuthenticateHandler({ model: model, scope: 'foobar' }); + var grantType = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foobar' + }); grantType.scope.should.equal('foobar'); }); @@ -94,8 +122,9 @@ describe('AuthenticateHandler integration', function() { }; 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) + return handler.handle(request, response) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(AccessDeniedError); @@ -111,8 +140,9 @@ describe('AuthenticateHandler integration', function() { }; 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) + return handler.handle(request, response) .then(should.fail) .catch(function(e) { e.should.be.an.instanceOf(ServerError); @@ -130,15 +160,16 @@ describe('AuthenticateHandler integration', function() { return true; } }; - var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + 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) + return handler.handle(request, response) .then(function(data) { data.should.equal(accessToken); }) @@ -388,7 +419,7 @@ describe('AuthenticateHandler integration', function() { return false; } }; - var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); return handler.validateScope('foo') .then(should.fail) @@ -405,7 +436,7 @@ describe('AuthenticateHandler integration', function() { return true; } }; - var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); handler.validateScope('foo').should.be.an.instanceOf(Promise); }); @@ -417,9 +448,37 @@ describe('AuthenticateHandler integration', function() { return true; } }; - var handler = new AuthenticateHandler({ model: model, scope: 'foo' }); + var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); handler.validateScope('foo').should.be.an.instanceOf(Promise); }); }); + + describe('updateResponse()', function() { + it('should set the `X-Accepted-OAuth-Scopes` header', function() { + var model = { + getAccessToken: function() {}, + validateScope: 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 set the `X-Authorized-OAuth-Scopes` header', function() { + var model = { + getAccessToken: function() {}, + validateScope: 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_test.js b/test/integration/handlers/authorize-handler_test.js index aeb64e751..b926a2651 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -737,8 +737,9 @@ describe('AuthorizeHandler integration', 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) + return handler.getUser(request, response) .then(function(data) { data.should.equal(user); }) diff --git a/test/integration/server_test.js b/test/integration/server_test.js index d9e76a468..bdc283f3e 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -36,6 +36,24 @@ describe('Server integration', function() { }); describe('authenticate()', function() { + it('should set the default `options`', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + } + }; + 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; + }) + .catch(should.fail); + }); + it('should return a promise', function() { var model = { getAccessToken: function() { @@ -44,7 +62,8 @@ describe('Server integration', function() { }; var server = new Server({ model: model }); var request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - var handler = server.authenticate(request); + var response = new Response({ body: {}, headers: {} }); + var handler = server.authenticate(request, response); handler.should.be.an.instanceOf(Promise); }); @@ -57,13 +76,14 @@ describe('Server integration', function() { }; 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, null, next); + server.authenticate(request, response, null, next); }); }); describe('authorize()', function() { - it('should set the default `authorizationCodeLifetime`', function() { + it('should set the default `options`', function() { var model = { getAccessToken: function() { return { user: {} }; @@ -127,7 +147,7 @@ describe('Server integration', function() { }); describe('token()', function() { - it('should set the default `accessTokenLifetime`', function() { + it('should set the default `options`', function() { var model = { getClient: function() { return { grants: ['password'] }; @@ -146,28 +166,6 @@ describe('Server integration', function() { return server.token(request, response) .then(function() { this.accessTokenLifetime.should.equal(3600); - }) - .catch(should.fail); - }); - - it('should set the default `refreshTokenLifetime`', 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: {} }); - - return server.token(request, response) - .then(function() { this.refreshTokenLifetime.should.equal(1209600); }) .catch(should.fail); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index 41e1fd49e..c7731169e 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -98,7 +98,7 @@ describe('AuthenticateHandler', function() { getAccessToken: function() {}, validateScope: sinon.stub().returns(true) }; - var handler = new AuthenticateHandler({ model: model, scope: 'bar' }); + var handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'bar' }); return handler.validateScope('foo') .then(function() { From 90871aa893b9c180badbab94b953fe9bd7787862 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 26 May 2015 15:56:03 +0100 Subject: [PATCH 20/39] Fix authorize handler now supports scope by query --- lib/handlers/authorize-handler.js | 8 +++-- .../handlers/authorize-handler_test.js | 34 ++++++++++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index e08ba2ea5..32b8fd222 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -181,15 +181,17 @@ AuthorizeHandler.prototype.getClient = function(request) { }; /** - * Get scope from the request body. + * Get scope from the request. */ AuthorizeHandler.prototype.getScope = function(request) { - if (!is.nqschar(request.body.scope)) { + var scope = request.body.scope || request.query.scope; + + if (!is.nqschar(scope)) { throw new InvalidArgumentError('Invalid parameter: `scope`'); } - return request.body.scope; + return scope; }; /** diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index b926a2651..a2595d3ac 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -644,16 +644,32 @@ describe('AuthorizeHandler integration', 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: {} }); + 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'); + 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'); + }); }); }); From 95bd4be59e5842ca0c83d4b6ea7c184c5dcd4a51 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Fri, 29 May 2015 12:35:21 +0100 Subject: [PATCH 21/39] Fix missing header on unauthorized requests --- lib/handlers/authenticate-handler.js | 8 ++++++++ .../handlers/authenticate-handler_test.js | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 0f71c42ca..84da91275 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -81,6 +81,14 @@ AuthenticateHandler.prototype.handle = function(request, response) { 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); } diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index cd6fd5c34..6451ee98c 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -114,6 +114,23 @@ describe('AuthenticateHandler integration', function() { } }); + 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() { From 6b182e146be9114eb3d0fdfa5fc0ee87dc61e552 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Fri, 29 May 2015 12:35:47 +0100 Subject: [PATCH 22/39] Remove examples --- examples/dynamodb/Readme.md | 138 ------------------------------ examples/dynamodb/aws.json | 1 - examples/dynamodb/dal.js | 94 -------------------- examples/dynamodb/index.js | 88 ------------------- examples/dynamodb/model.js | 152 --------------------------------- examples/memory/Readme.md | 29 ------- examples/memory/model.js | 111 ------------------------ examples/mongodb/Readme.md | 37 -------- examples/mongodb/model.js | 137 ----------------------------- examples/postgresql/Readme.md | 25 ------ examples/postgresql/index.js | 88 ------------------- examples/postgresql/model.js | 128 --------------------------- examples/postgresql/schema.sql | 119 -------------------------- examples/redis/README.md | 19 ----- examples/redis/index.js | 33 ------- examples/redis/model.js | 90 ------------------- examples/redis/testData.js | 27 ------ 17 files changed, 1316 deletions(-) delete mode 100644 examples/dynamodb/Readme.md delete mode 100644 examples/dynamodb/aws.json delete mode 100644 examples/dynamodb/dal.js delete mode 100644 examples/dynamodb/index.js delete mode 100644 examples/dynamodb/model.js delete mode 100644 examples/memory/Readme.md delete mode 100644 examples/memory/model.js delete mode 100755 examples/mongodb/Readme.md delete mode 100755 examples/mongodb/model.js delete mode 100644 examples/postgresql/Readme.md delete mode 100644 examples/postgresql/index.js delete mode 100644 examples/postgresql/model.js delete mode 100644 examples/postgresql/schema.sql delete mode 100644 examples/redis/README.md delete mode 100644 examples/redis/index.js delete mode 100644 examples/redis/model.js delete mode 100644 examples/redis/testData.js diff --git a/examples/dynamodb/Readme.md b/examples/dynamodb/Readme.md deleted file mode 100644 index 83e41fb4b..000000000 --- a/examples/dynamodb/Readme.md +++ /dev/null @@ -1,138 +0,0 @@ -# DynamoDB Example - -requires [`aws-sdk`](http://aws.amazon.com/sdkfornodejs/) - -You will need to create the required tables (see below): - -The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. - -For example: - -```js -... - -app.oauth = oauthserver({ - model: require('./model'), - grants: ['password', 'refresh_token'], - debug: true -}); - -... -``` - - -#### Creating required tables in DynamoDB - -```js -// -// Table definitions -// -var OAuth2AccessToken = { - AttributeDefinitions: [ - { - AttributeName: "accessToken", - AttributeType: "S" - } - ], - TableName: "oauth2accesstoken", - KeySchema: [ - { - AttributeName: "accessToken", - KeyType: "HASH" - } - ], - ProvisionedThroughput: { - ReadCapacityUnits: 12, - WriteCapacityUnits: 6 - } -}; - -var OAuth2RefreshToken = { - AttributeDefinitions: [ - { - AttributeName: "refreshToken", - AttributeType: "S" - } - ], - TableName: "oauth2refreshtoken", - KeySchema: [ - { - AttributeName: "refreshToken", - KeyType: "HASH" - } - ], - ProvisionedThroughput: { - ReadCapacityUnits: 6, - WriteCapacityUnits: 6 - } -}; - -var OAuth2AuthCode = { - AttributeDefinitions: [ - { - AttributeName: "authCode", - AttributeType: "S" - } - ], - TableName: "oauth2authcode", - KeySchema: [ - { - AttributeName: "authCode", - KeyType: "HASH" - } - ], - ProvisionedThroughput: { - ReadCapacityUnits: 6, - WriteCapacityUnits: 6 - } -}; - -var OAuth2Client = { - AttributeDefinitions: [ - { - AttributeName: "clientId", - AttributeType: "S" - } - ], - TableName: "oauth2client", - KeySchema: [ - { - AttributeName: "clientId", - KeyType: "HASH" - } - ], - ProvisionedThroughput: { - ReadCapacityUnits: 6, - WriteCapacityUnits: 6 - } -}; - - -var OAuth2User = { - AttributeDefinitions: [ - { - AttributeName: "username", - AttributeType: "S" - }, - { - AttributeName: "password", - AttributeType: "S" - } - ], - TableName: "oauth2user", - KeySchema: [ - { - AttributeName: "username", - KeyType: "HASH" - }, - { - AttributeName: "password", - KeyType: "RANGE" - } - ], - ProvisionedThroughput: { - ReadCapacityUnits: 6, - WriteCapacityUnits: 6 - } -}; -``` diff --git a/examples/dynamodb/aws.json b/examples/dynamodb/aws.json deleted file mode 100644 index cc2f44d5f..000000000 --- a/examples/dynamodb/aws.json +++ /dev/null @@ -1 +0,0 @@ -{ "accessKeyId": "YOUR ACCESS KEY", "secretAccessKey": "YOUR SECRET KEY", "region": "us-east-1" } diff --git a/examples/dynamodb/dal.js b/examples/dynamodb/dal.js deleted file mode 100644 index 5400aa610..000000000 --- a/examples/dynamodb/dal.js +++ /dev/null @@ -1,94 +0,0 @@ -var Dal = function () { - this.AWS = require('aws-sdk'); - this.AWS.config.loadFromPath(__dirname + '/aws.json'); - //change the endpoint to match your dynamodb endpoint - this.db = new this.AWS.DynamoDB({ - endpoint: "https://dynamodb.us-east-1.amazonaws.com/" - }); -}; - -Dal.prototype = { - decodeValue: function (map) { - if (typeof map.N !== 'undefined') - return parseFloat(map.N); - if (typeof map.S !== 'undefined') - return map.S; - return map.Bl; - }, - decodeValues: function (obj, vals) { - var self = this; - for (var key in vals) { - if (vals.hasOwnProperty(key)) { - obj[key] = self.decodeValue(vals[key]); - } - } - }, - formatAttributes: function (obj) { - var item = {}; - for (var p in obj) { - if (obj.hasOwnProperty(p)) { - if (obj[p] === 0 || typeof obj[p] == "number") { - item[p] = {"N": obj[p].toString()}; - } - else { - item[p] = {"S": obj[p]}; - } - } - } - return item; - }, - deleteEmptyProperties: function (obj) { - //DynamoDB does not allow you to store empty values - for (var p in obj) { - if (!obj.hasOwnProperty(p)) - continue; - if (obj[p] !== 0 && (obj[p] === null || obj[p] === "")) { - delete(obj[p]); - } - } - }, - doGet: function (tableName, keyHash, consistent, callback) { - consistent = typeof consistent !== 'undefined' ? consistent : false; - var self = this; - var result = this.db.getItem({ - TableName: tableName, - Key: keyHash, - ConsistentRead: consistent - }, function (err, data) { - var obj = {}; - if (err !== null) { - if (callback) callback(err, null); - return; - } - if (typeof data.Item != "undefined") { - self.decodeValues(obj, data.Item); - } - if (callback) callback(err, obj); - }); - }, - doSet: function (obj, tableName, keyHash, callback) { - this.deleteEmptyProperties(obj); - this.db.putItem({ - TableName: tableName, - Item: this.formatAttributes(obj) - }, function (err, data) { - - if (err !== null) return callback(err, null); - callback(err, obj); - }); - }, - doDelete: function (tableName, keyHash, callback) { - this.db.deleteItem({ - TableName: tableName, - Key: keyHash - }, - function (err, data) { - if (err !== null) { - console.log(err); - } - if (callback) callback(err, data); - }); - } -}; - -module.exports = new Dal(); diff --git a/examples/dynamodb/index.js b/examples/dynamodb/index.js deleted file mode 100644 index 059d7af6a..000000000 --- a/examples/dynamodb/index.js +++ /dev/null @@ -1,88 +0,0 @@ -var express = require('express'), - bodyParser = require('body-parser'), - oauthserver = require('../../'); // Would be: 'oauth2-server' - -var app = express(); - -app.use(bodyParser.urlencoded({ extended: true })); - -app.use(bodyParser.json()); - -app.oauth = oauthserver({ - model: require('./model'), - grants: ['password', 'refresh_token'], - debug: true -}); - -// Handle token grant requests -app.all('/oauth/token', app.oauth.grant()); - -// Show them the "do you authorise xyz app to access your content?" page -app.get('/oauth/authorise', function (req, res, next) { - if (!req.session.user) { - // If they aren't logged in, send them to your own login implementation - return res.redirect('/login?redirect=' + req.path + '&client_id=' + - req.query.client_id + '&redirect_uri=' + req.query.redirect_uri); - } - - res.render('authorise', { - client_id: req.query.client_id, - redirect_uri: req.query.redirect_uri - }); -}); - -// Handle authorise -app.post('/oauth/authorise', function (req, res, next) { - if (!req.session.user) { - return res.redirect('/login?client_id=' + req.query.client_id + - '&redirect_uri=' + req.query.redirect_uri); - } - - next(); -}, app.oauth.authCodeGrant(function (req, next) { - // The first param should to indicate an error - // The second param should a bool to indicate if the user did authorise the app - // The third param should for the user/uid (only used for passing to saveAuthCode) - next(null, req.body.allow === 'yes', req.session.user.id, req.session.user); -})); - -// Show login -app.get('/login', function (req, res, next) { - res.render('login', { - redirect: req.query.redirect, - client_id: req.query.client_id, - redirect_uri: req.query.redirect_uri - }); -}); - -// Handle login -app.post('/login', function (req, res, next) { - // Insert your own login mechanism - if (req.body.email !== 'thom@nightworld.com') { - res.render('login', { - redirect: req.body.redirect, - client_id: req.body.client_id, - redirect_uri: req.body.redirect_uri - }); - } else { - // Successful logins should send the user back to the /oauth/authorise - // with the client_id and redirect_uri (you could store these in the session) - return res.redirect((req.body.redirect || '/home') + '?client_id=' + - req.body.client_id + '&redirect_uri=' + req.body.redirect_uri); - } -}); - -app.get('/secret', app.oauth.authorise(), function (req, res) { - // Will require a valid access_token - res.send('Secret area'); -}); - -app.get('/public', function (req, res) { - // Does not require an access_token - res.send('Public area'); -}); - -// Error handling -app.use(app.oauth.errorHandler()); - -app.listen(3000); diff --git a/examples/dynamodb/model.js b/examples/dynamodb/model.js deleted file mode 100644 index 8b3a67ee5..000000000 --- a/examples/dynamodb/model.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var dal = require('./dal.js'); -model = module.exports; - -var OAuthAccessTokenTable = "oauth2accesstoken"; -var OAuthAuthCodeTable = "oauth2authcode"; -var OAuthRefreshTokenTable = "oauth2refreshtoken"; -var OAuthClientTable = "oauth2client"; -var OAuthUserTable = "userid_map"; - -// -// oauth2-server callbacks -// -model.getAccessToken = function (bearerToken, callback) { - console.log('in getAccessToken (bearerToken: ' + bearerToken + ')'); - - dal.doGet(OAuthAccessTokenTable, - {"accessToken": {"S": bearerToken}}, true, function(err, data) { - if (data && data.expires) { - data.expires = new Date(data.expires * 1000); - } - callback(err, data); - }); -}; - -model.getClient = function (clientId, clientSecret, callback) { - console.log('in getClient (clientId: ' + clientId + ', clientSecret: ' + clientSecret + ')'); - dal.doGet(OAuthClientTable, { clientId: { S: clientId }}, true, - function(err, data) { - if (err || !data) return callback(err, data); - - if (clientSecret !== null && data.clientSecret !== clientSecret) { - return callback(); - } - - callback(null, data); - }); -}; - -// This will very much depend on your setup, I wouldn't advise doing anything exactly like this but -// it gives an example of how to use the method to restrict certain grant types -var authorizedClientIds = ['abc1', 'def2']; -model.grantTypeAllowed = function (clientId, grantType, callback) { - console.log('in grantTypeAllowed (clientId: ' + clientId + ', grantType: ' + grantType + ')'); - - if (grantType === 'password') { - return callback(false, authorizedClientIds.indexOf(clientId) >= 0); - } - - callback(false, true); -}; - -model.saveAccessToken = function (accessToken, clientId, expires, user, callback) { - console.log('in saveAccessToken (accessToken: ' + accessToken + ', clientId: ' + clientId + ', userId: ' + user.id + ', expires: ' + expires + ')'); - - var token = { - accessToken: accessToken, - clientId: clientId, - userId: user.id - }; - - if (expires) token.expires = parseInt(expires / 1000, 10); - console.log('saving', token); - - dal.doSet(token, OAuthAccessTokenTable, { accessToken: { S: accessToken }}, callback); -}; - -model.saveRefreshToken = function (refreshToken, clientId, expires, user, callback) { - console.log('in saveRefreshToken (refreshToken: ' + refreshToken + ', clientId: ' + clientId + ', userId: ' + user.id + ', expires: ' + expires + ')'); - - var token = { - refreshToken: refreshToken, - clientId: clientId, - userId: user.id - }; - - if (expires) token.expires = parseInt(expires / 1000, 10); - console.log('saving', token); - - dal.doSet(token, OAuthRefreshTokenTable, { refreshToken: { S: refreshToken }}, callback); -}; - -model.getRefreshToken = function (bearerToken, callback) { - console.log("in getRefreshToken (bearerToken: " + bearerToken + ")"); - - dal.doGet(OAuthRefreshTokenTable, { refreshToken: { S: bearerToken }}, true, function(err, data) { - if (data && data.expires) { - data.expires = new Date(data.expires * 1000); - } - callback(err, data); - }); -}; - -model.revokeRefreshToken = function(bearerToken, callback) { - console.log("in revokeRefreshToken (bearerToken: " + bearerToken + ")"); - - dal.doDelete(OAuthRefreshTokenTable, { refreshToken: { S: bearerToken }}, callback); -}; - -model.getAuthCode = function (bearerCode, callback) { - console.log("in getAuthCode (bearerCode: " + bearerCode + ")"); - - dal.doGet(OAuthAuthCodeTable, { authCode: { S: bearerCode }}, true, function(err, data) { - if (data && data.expires) { - data.expires = new Date(data.expires * 1000); - } - callback(err, data); - }); -}; - -model.saveAuthCode = function (authCode, clientId, expires, user, callback) { - console.log('in saveAuthCode (authCode: ' + authCode + ', clientId: ' + clientId + ', userId: ' + user.id + ', expires: ' + expires + ')'); - - var code = { - authCode: authCode, - clientId: clientId, - userId: user.id - }; - - if (expires) code.expires = parseInt(expires / 1000, 10); - console.log("saving", code); - - dal.doSet(code, OAuthAuthCodeTable, { authCode: { S: authCode }}, callback); -}; - - -/* - * Required to support password grant type - */ -model.getUser = function (username, password, callback) { - console.log('in getUser (username: ' + username + ', password: ' + password + ')'); - - dal.doGet(OAuthUserTable, { id: { S: "email:" + username}}, true, function(err, data) { - if (err) return callback(err); - callback(null, { id: data.userId }); - }); -}; diff --git a/examples/memory/Readme.md b/examples/memory/Readme.md deleted file mode 100644 index 96b658ed0..000000000 --- a/examples/memory/Readme.md +++ /dev/null @@ -1,29 +0,0 @@ -# In-Memory Example - -## DO NOT USE THIS EXAMPLE IN PRODUCTION - -The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. - -For example: - -```js - -var memorystore = require('model.js'); - -app.oauth = oauthserver({ - model: memorystore, - grants: ['password','refresh_token'], - debug: true -}); - -``` - -# Dump - -You can also dump the contents of the memory store (for debugging) like so: - -```js - -memorystore.dump(); - -``` diff --git a/examples/memory/model.js b/examples/memory/model.js deleted file mode 100644 index b523d982e..000000000 --- a/examples/memory/model.js +++ /dev/null @@ -1,111 +0,0 @@ -var model = module.exports; - -// In-memory datastores: -var oauthAccessTokens = [], - oauthRefreshTokens = [], - oauthClients = [ - { - clientId : 'thom', - clientSecret : 'nightworld', - redirectUri : '' - } - ], - authorizedClientIds = { - password: [ - 'thom' - ], - refresh_token: [ - 'thom' - ] - }, - users = [ - { - id : '123', - username: 'thomseddon', - password: 'nightworld' - } - ]; - -// Debug function to dump the state of the data stores -model.dump = function() { - console.log('oauthAccessTokens', oauthAccessTokens); - console.log('oauthClients', oauthClients); - console.log('authorizedClientIds', authorizedClientIds); - console.log('oauthRefreshTokens', oauthRefreshTokens); - console.log('users', users); -}; - -/* - * Required - */ - -model.getAccessToken = function (bearerToken, callback) { - for(var i = 0, len = oauthAccessTokens.length; i < len; i++) { - var elem = oauthAccessTokens[i]; - if(elem.accessToken === bearerToken) { - return callback(false, elem); - } - } - callback(false, false); -}; - -model.getRefreshToken = function (bearerToken, callback) { - for(var i = 0, len = oauthRefreshTokens.length; i < len; i++) { - var elem = oauthRefreshTokens[i]; - if(elem.refreshToken === bearerToken) { - return callback(false, elem); - } - } - callback(false, false); -}; - -model.getClient = function (clientId, clientSecret, callback) { - for(var i = 0, len = oauthClients.length; i < len; i++) { - var elem = oauthClients[i]; - if(elem.clientId === clientId && - (clientSecret === null || elem.clientSecret === clientSecret)) { - return callback(false, elem); - } - } - callback(false, false); -}; - -model.grantTypeAllowed = function (clientId, grantType, callback) { - callback(false, authorizedClientIds[grantType] && - authorizedClientIds[grantType].indexOf(clientId.toLowerCase()) >= 0); -}; - -model.saveAccessToken = function (accessToken, clientId, expires, userId, callback) { - oauthAccessTokens.unshift({ - accessToken: accessToken, - clientId: clientId, - userId: userId, - expires: expires - }); - - callback(false); -}; - -model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { - oauthRefreshTokens.unshift({ - refreshToken: refreshToken, - clientId: clientId, - userId: userId, - expires: expires - }); - - callback(false); -}; - -/* - * Required to support password grant type - */ -model.getUser = function (username, password, callback) { - for(var i = 0, len = users.length; i < len; i++) { - var elem = users[i]; - if(elem.username === username && elem.password === password) { - return callback(false, elem); - } - } - callback(false, false); -}; diff --git a/examples/mongodb/Readme.md b/examples/mongodb/Readme.md deleted file mode 100755 index 3a8295bf6..000000000 --- a/examples/mongodb/Readme.md +++ /dev/null @@ -1,37 +0,0 @@ -# MongoDB Example - -You will need to initialize a Mongoose connection to a mongo db beforehand. - -For example : - -```js - -var mongoose = require('mongoose'); - -var uristring = 'mongodb://localhost/test'; - -// Makes connection asynchronously. Mongoose will queue up database -// operations and release them when the connection is complete. -mongoose.connect(uristring, function (err, res) { - if (err) { - console.log ('ERROR connecting to: ' + uristring + '. ' + err); - } else { - console.log ('Succeeded connected to: ' + uristring); - } -}); - -``` - -The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. - -For example: - -```js - -app.oauth = oauthserver({ - model: require('./model'), - grants: ['password'], - debug: true -}); - -``` \ No newline at end of file diff --git a/examples/mongodb/model.js b/examples/mongodb/model.js deleted file mode 100755 index d0a2f037a..000000000 --- a/examples/mongodb/model.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var mongoose = require('mongoose'), - Schema = mongoose.Schema, - model = module.exports; - -// -// Schemas definitions -// -var OAuthAccessTokensSchema = new Schema({ - accessToken: { type: String }, - clientId: { type: String }, - userId: { type: String }, - expires: { type: Date } -}); - -var OAuthRefreshTokensSchema = new Schema({ - refreshToken: { type: String }, - clientId: { type: String }, - userId: { type: String }, - expires: { type: Date } -}); - -var OAuthClientsSchema = new Schema({ - clientId: { type: String }, - clientSecret: { type: String }, - redirectUri: { type: String } -}); - -var OAuthUsersSchema = new Schema({ - username: { type: String }, - password: { type: String }, - firstname: { type: String }, - lastname: { type: String }, - email: { type: String, default: '' } -}); - -mongoose.model('OAuthAccessTokens', OAuthAccessTokensSchema); -mongoose.model('OAuthRefreshTokens', OAuthRefreshTokensSchema); -mongoose.model('OAuthClients', OAuthClientsSchema); -mongoose.model('OAuthUsers', OAuthUsersSchema); - -var OAuthAccessTokensModel = mongoose.model('OAuthAccessTokens'), - OAuthRefreshTokensModel = mongoose.model('OAuthRefreshTokens'), - OAuthClientsModel = mongoose.model('OAuthClients'), - OAuthUsersModel = mongoose.model('OAuthUsers'); - -// -// oauth2-server callbacks -// -model.getAccessToken = function (bearerToken, callback) { - console.log('in getAccessToken (bearerToken: ' + bearerToken + ')'); - - OAuthAccessTokensModel.findOne({ accessToken: bearerToken }, callback); -}; - -model.getClient = function (clientId, clientSecret, callback) { - console.log('in getClient (clientId: ' + clientId + ', clientSecret: ' + clientSecret + ')'); - if (clientSecret === null) { - return OAuthClientsModel.findOne({ clientId: clientId }, callback); - } - OAuthClientsModel.findOne({ clientId: clientId, clientSecret: clientSecret }, callback); -}; - -// This will very much depend on your setup, I wouldn't advise doing anything exactly like this but -// it gives an example of how to use the method to resrict certain grant types -var authorizedClientIds = ['s6BhdRkqt3', 'toto']; -model.grantTypeAllowed = function (clientId, grantType, callback) { - console.log('in grantTypeAllowed (clientId: ' + clientId + ', grantType: ' + grantType + ')'); - - if (grantType === 'password') { - return callback(false, authorizedClientIds.indexOf(clientId) >= 0); - } - - callback(false, true); -}; - -model.saveAccessToken = function (token, clientId, expires, userId, callback) { - console.log('in saveAccessToken (token: ' + token + ', clientId: ' + clientId + ', userId: ' + userId + ', expires: ' + expires + ')'); - - var accessToken = new OAuthAccessTokensModel({ - accessToken: token, - clientId: clientId, - userId: userId, - expires: expires - }); - - accessToken.save(callback); -}; - -/* - * Required to support password grant type - */ -model.getUser = function (username, password, callback) { - console.log('in getUser (username: ' + username + ', password: ' + password + ')'); - - OAuthUsersModel.findOne({ username: username, password: password }, function(err, user) { - if(err) return callback(err); - callback(null, user._id); - }); -}; - -/* - * Required to support refreshToken grant type - */ -model.saveRefreshToken = function (token, clientId, expires, userId, callback) { - console.log('in saveRefreshToken (token: ' + token + ', clientId: ' + clientId +', userId: ' + userId + ', expires: ' + expires + ')'); - - var refreshToken = new OAuthRefreshTokensModel({ - refreshToken: token, - clientId: clientId, - userId: userId, - expires: expires - }); - - refreshToken.save(callback); -}; - -model.getRefreshToken = function (refreshToken, callback) { - console.log('in getRefreshToken (refreshToken: ' + refreshToken + ')'); - - OAuthRefreshTokensModel.findOne({ refreshToken: refreshToken }, callback); -}; diff --git a/examples/postgresql/Readme.md b/examples/postgresql/Readme.md deleted file mode 100644 index 64534527f..000000000 --- a/examples/postgresql/Readme.md +++ /dev/null @@ -1,25 +0,0 @@ -# PostgreSQL Example - -See schema.sql for the tables referred to in this example - -The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. - -For example: - -```js - -var oauth = oauthserver({ - model: require('./model'), - grants: ['password'], - debug: true -}); - -``` - -## Note - -In this example, the postgres connection info is read from the `DATABASE_URL` environment variable which you can set when you run, for example: - -``` -$ DATABASE_URL=postgres://postgres:1234@localhost/postgres node index.js -``` \ No newline at end of file diff --git a/examples/postgresql/index.js b/examples/postgresql/index.js deleted file mode 100644 index 1c9af863f..000000000 --- a/examples/postgresql/index.js +++ /dev/null @@ -1,88 +0,0 @@ -var express = require('express'), - bodyParser = require('body-parser'), - oauthserver = require('../../'); // Would be: 'oauth2-server' - -var app = express(); - -app.use(bodyParser.urlencoded({ extended: true })); - -app.use(bodyParser.json()); - -app.oauth = oauthserver({ - model: require('./model'), - grants: ['auth_code', 'password'], - debug: true -}); - -// Handle token grant requests -app.all('/oauth/token', app.oauth.grant()); - -// Show them the "do you authorise xyz app to access your content?" page -app.get('/oauth/authorise', function (req, res, next) { - if (!req.session.user) { - // If they aren't logged in, send them to your own login implementation - return res.redirect('/login?redirect=' + req.path + '&client_id=' + - req.query.client_id + '&redirect_uri=' + req.query.redirect_uri); - } - - res.render('authorise', { - client_id: req.query.client_id, - redirect_uri: req.query.redirect_uri - }); -}); - -// Handle authorise -app.post('/oauth/authorise', function (req, res, next) { - if (!req.session.user) { - return res.redirect('/login?client_id=' + req.query.client_id + - '&redirect_uri=' + req.query.redirect_uri); - } - - next(); -}, app.oauth.authCodeGrant(function (req, next) { - // The first param should to indicate an error - // The second param should a bool to indicate if the user did authorise the app - // The third param should for the user/uid (only used for passing to saveAuthCode) - next(null, req.body.allow === 'yes', req.session.user.id, req.session.user); -})); - -// Show login -app.get('/login', function (req, res, next) { - res.render('login', { - redirect: req.query.redirect, - client_id: req.query.client_id, - redirect_uri: req.query.redirect_uri - }); -}); - -// Handle login -app.post('/login', function (req, res, next) { - // Insert your own login mechanism - if (req.body.email !== 'thom@nightworld.com') { - res.render('login', { - redirect: req.body.redirect, - client_id: req.body.client_id, - redirect_uri: req.body.redirect_uri - }); - } else { - // Successful logins should send the user back to the /oauth/authorise - // with the client_id and redirect_uri (you could store these in the session) - return res.redirect((req.body.redirect || '/home') + '?client_id=' + - req.body.client_id + '&redirect_uri=' + req.body.redirect_uri); - } -}); - -app.get('/secret', app.oauth.authorise(), function (req, res) { - // Will require a valid access_token - res.send('Secret area'); -}); - -app.get('/public', function (req, res) { - // Does not require an access_token - res.send('Public area'); -}); - -// Error handling -app.use(app.oauth.errorHandler()); - -app.listen(3000); diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js deleted file mode 100644 index 31df5369c..000000000 --- a/examples/postgresql/model.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright 2013-present NightWorld. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var pg = require('pg'), - model = module.exports, - connString = process.env.DATABASE_URL; - -/* - * Required - */ - -model.getAccessToken = function (bearerToken, callback) { - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - client.query('SELECT access_token, client_id, expires, user_id FROM oauth_access_tokens ' + - 'WHERE access_token = $1', [bearerToken], function (err, result) { - if (err || !result.rowCount) return callback(err); - // This object will be exposed in req.oauth.token - // The user_id field will be exposed in req.user (req.user = { id: "..." }) however if - // an explicit user object is included (token.user, must include id) it will be exposed - // in req.user instead - var token = result.rows[0]; - callback(null, { - accessToken: token.access_token, - clientId: token.client_id, - expires: token.expires, - userId: token.userId - }); - done(); - }); - }); -}; - -model.getClient = function (clientId, clientSecret, callback) { - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - - client.query('SELECT client_id, client_secret, redirect_uri FROM oauth_clients WHERE ' + - 'client_id = $1', [clientId], function (err, result) { - if (err || !result.rowCount) return callback(err); - - var client = result.rows[0]; - - if (clientSecret !== null && client.client_secret !== clientSecret) return callback(); - - // This object will be exposed in req.oauth.client - callback(null, { - clientId: client.client_id, - clientSecret: client.client_secret - }); - done(); - }); - }); -}; - -model.getRefreshToken = function (bearerToken, callback) { - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - client.query('SELECT refresh_token, client_id, expires, user_id FROM oauth_refresh_tokens ' + - 'WHERE refresh_token = $1', [bearerToken], function (err, result) { - // The returned user_id will be exposed in req.user.id - callback(err, result.rowCount ? result.rows[0] : false); - done(); - }); - }); -}; - -// This will very much depend on your setup, I wouldn't advise doing anything exactly like this but -// it gives an example of how to use the method to resrict certain grant types -var authorizedClientIds = ['abc1', 'def2']; -model.grantTypeAllowed = function (clientId, grantType, callback) { - if (grantType === 'password') { - return callback(false, authorizedClientIds.indexOf(clientId.toLowerCase()) >= 0); - } - - callback(false, true); -}; - -model.saveAccessToken = function (accessToken, clientId, expires, userId, callback) { - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - client.query('INSERT INTO oauth_access_tokens(access_token, client_id, user_id, expires) ' + - 'VALUES ($1, $2, $3, $4)', [accessToken, clientId, userId, expires], - function (err, result) { - callback(err); - done(); - }); - }); -}; - -model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, user_id, ' + - 'expires) VALUES ($1, $2, $3, $4)', [refreshToken, clientId, userId, expires], - function (err, result) { - callback(err); - done(); - }); - }); -}; - -/* - * Required to support password grant type - */ -model.getUser = function (username, password, callback) { - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - client.query('SELECT id FROM users WHERE username = $1 AND password = $2', [username, - password], function (err, result) { - callback(err, result.rowCount ? result.rows[0] : false); - done(); - }); - }); -}; diff --git a/examples/postgresql/schema.sql b/examples/postgresql/schema.sql deleted file mode 100644 index d31212a8a..000000000 --- a/examples/postgresql/schema.sql +++ /dev/null @@ -1,119 +0,0 @@ --- --- PostgreSQL database dump --- - -SET statement_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SET check_function_bodies = false; -SET client_min_messages = warning; - --- --- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; - - --- --- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; - - -SET search_path = public, pg_catalog; - -SET default_tablespace = ''; - -SET default_with_oids = false; - --- --- Name: oauth_access_tokens; Type: TABLE; Schema: public; Owner: -; Tablespace: --- - -CREATE TABLE oauth_access_tokens ( - access_token text NOT NULL, - client_id text NOT NULL, - user_id uuid NOT NULL, - expires timestamp without time zone NOT NULL -); - - --- --- Name: oauth_clients; Type: TABLE; Schema: public; Owner: -; Tablespace: --- - -CREATE TABLE oauth_clients ( - client_id text NOT NULL, - client_secret text NOT NULL, - redirect_uri text NOT NULL -); - - --- --- Name: oauth_refresh_tokens; Type: TABLE; Schema: public; Owner: -; Tablespace: --- - -CREATE TABLE oauth_refresh_tokens ( - refresh_token text NOT NULL, - client_id text NOT NULL, - user_id uuid NOT NULL, - expires timestamp without time zone NOT NULL -); - - --- --- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace: --- - -CREATE TABLE users ( - id uuid NOT NULL, - username text NOT NULL, - password text NOT NULL -); - - --- --- Name: oauth_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: --- - -ALTER TABLE ONLY oauth_access_tokens - ADD CONSTRAINT oauth_access_tokens_pkey PRIMARY KEY (access_token); - - --- --- Name: oauth_clients_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: --- - -ALTER TABLE ONLY oauth_clients - ADD CONSTRAINT oauth_clients_pkey PRIMARY KEY (client_id, client_secret); - - --- --- Name: oauth_refresh_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: --- - -ALTER TABLE ONLY oauth_refresh_tokens - ADD CONSTRAINT oauth_refresh_tokens_pkey PRIMARY KEY (refresh_token); - - --- --- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: --- - -ALTER TABLE ONLY users - ADD CONSTRAINT users_pkey PRIMARY KEY (id); - - --- --- Name: users_username_password; Type: INDEX; Schema: public; Owner: -; Tablespace: --- - -CREATE INDEX users_username_password ON users USING btree (username, password); - - --- --- PostgreSQL database dump complete --- - diff --git a/examples/redis/README.md b/examples/redis/README.md deleted file mode 100644 index 87c17951a..000000000 --- a/examples/redis/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Redis Example - -A simple example with support for `password` and `refresh_token` grants using [Redis](http://redis.io/). You'll need [node-redis](https://github.com/mranney/node_redis) installed. - -## Usage - -```js -app.oauth = oauthserver({ - model: require('./model'), - grants: ['password', 'refresh_token'], - debug: true -}); -``` - -## Data model - -The example makes use of a simple data model where clients, tokens, refresh tokens and users are stored as [hashes](http://redis.io/topics/data-types#hashes). The allowed grants for each client are stored in a [set](http://redis.io/topics/data-types#sets) `clients:{id}:grant_types`. This allows grants to be added or removed dynamically. To simplify the user lookup users are identified by their username and not by a separate ID. Passwords are stored in the clear for simplicity, but in practice these should be hashed using a library like [bcrypt](https://github.com/ncb000gt/node.bcrypt.js). - -To inject some test data you can run the `testData.js` script in this directory. This will create a client with the ID `client` and secret `secret` and create a single user with the username `username` and password `password`. \ No newline at end of file diff --git a/examples/redis/index.js b/examples/redis/index.js deleted file mode 100644 index 8422cac1e..000000000 --- a/examples/redis/index.js +++ /dev/null @@ -1,33 +0,0 @@ -var express = require('express'), - bodyParser = require('body-parser'), - oauthserver = require('../../'); // Would be: 'oauth2-server' - -var app = express(); - -app.use(bodyParser.urlencoded({ extended: true })); - -app.use(bodyParser.json()); - -app.oauth = oauthserver({ - model: require('./model'), - grants: ['password', 'refresh_token'], - debug: true -}); - -// Handle token grant requests -app.all('/oauth/token', app.oauth.grant()); - -app.get('/secret', app.oauth.authorise(), function (req, res) { - // Will require a valid access_token - res.send('Secret area'); -}); - -app.get('/public', function (req, res) { - // Does not require an access_token - res.send('Public area'); -}); - -// Error handling -app.use(app.oauth.errorHandler()); - -app.listen(3000); diff --git a/examples/redis/model.js b/examples/redis/model.js deleted file mode 100644 index 41eccf0e3..000000000 --- a/examples/redis/model.js +++ /dev/null @@ -1,90 +0,0 @@ -var model = module.exports, - util = require('util'), - redis = require('redis'); - -var db = redis.createClient(); - -var keys = { - token: 'tokens:%s', - client: 'clients:%s', - refreshToken: 'refresh_tokens:%s', - grantTypes: 'clients:%s:grant_types', - user: 'users:%s' -}; - -model.getAccessToken = function (bearerToken, callback) { - db.hgetall(util.format(keys.token, bearerToken), function (err, token) { - if (err) return callback(err); - - if (!token) return callback(); - - callback(null, { - accessToken: token.accessToken, - clientId: token.clientId, - expires: token.expires ? new Date(token.expires) : null, - userId: token.userId - }); - }); -}; - -model.getClient = function (clientId, clientSecret, callback) { - db.hgetall(util.format(keys.client, clientId), function (err, client) { - if (err) return callback(err); - - if (!client || client.clientSecret !== clientSecret) return callback(); - - callback(null, { - clientId: client.clientId, - clientSecret: client.clientSecret - }); - }); -}; - -model.getRefreshToken = function (bearerToken, callback) { - db.hgetall(util.format(keys.refreshToken, bearerToken), function (err, token) { - if (err) return callback(err); - - if (!token) return callback(); - - callback(null, { - refreshToken: token.accessToken, - clientId: token.clientId, - expires: token.expires ? new Date(token.expires) : null, - userId: token.userId - }); - }); -}; - -model.grantTypeAllowed = function (clientId, grantType, callback) { - db.sismember(util.format(keys.grantTypes, clientId), grantType, callback); -}; - -model.saveAccessToken = function (accessToken, clientId, expires, user, callback) { - db.hmset(util.format(keys.token, accessToken), { - accessToken: accessToken, - clientId: clientId, - expires: expires ? expires.toISOString() : null, - userId: user.id - }, callback); -}; - -model.saveRefreshToken = function (refreshToken, clientId, expires, user, callback) { - db.hmset(util.format(keys.refreshToken, refreshToken), { - refreshToken: refreshToken, - clientId: clientId, - expires: expires ? expires.toISOString() : null, - userId: user.id - }, callback); -}; - -model.getUser = function (username, password, callback) { - db.hgetall(util.format(keys.user, username), function (err, user) { - if (err) return callback(err); - - if (!user || password !== user.password) return callback(); - - callback(null, { - id: username - }); - }); -}; diff --git a/examples/redis/testData.js b/examples/redis/testData.js deleted file mode 100644 index c12afa877..000000000 --- a/examples/redis/testData.js +++ /dev/null @@ -1,27 +0,0 @@ -#! /usr/bin/env node - -var db = require('redis').createClient(); - -db.multi() - .hmset('users:username', { - id: 'username', - username: 'username', - password: 'password' - }) - .hmset('clients:client', { - clientId: 'client', - clientSecret: 'secret' - }) - .sadd('clients:client:grant_types', [ - 'password', - 'refresh_token' - ]) - .exec(function (errs) { - if (errs) { - console.error(errs[0].message); - return process.exit(1); - } - - console.log('Client and user added successfully'); - process.exit(); - }); From f7b91ab99bd29078c71803791c02f41630177a17 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 1 Jun 2015 16:52:10 +0100 Subject: [PATCH 23/39] Replace standard-error with standard-http-error --- lib/errors/invalid-argument-error.js | 6 +++--- lib/errors/oauth-error.js | 10 +++++++--- package.json | 4 ++-- test/integration/handlers/authorize-handler_test.js | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index a3c95abe4..91c450b5a 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -4,7 +4,7 @@ */ var _ = require('lodash'); -var StandardError = require('standard-error'); +var StandardHttpError = require('standard-http-error'); var util = require('util'); /** @@ -17,14 +17,14 @@ function InvalidArgumentError(message, properties) { name: 'invalid_argument' }, properties); - StandardError.call(this, message, properties); + StandardHttpError.call(this, properties.code, message, properties); } /** * Inherit prototype. */ -util.inherits(InvalidArgumentError, StandardError); +util.inherits(InvalidArgumentError, StandardHttpError); /** * Export constructor. diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js index ba2c11bac..d9fde2ee2 100644 --- a/lib/errors/oauth-error.js +++ b/lib/errors/oauth-error.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var StandardError = require('standard-error'); +var StandardHttpError = require('standard-http-error'); var util = require('util'); /** @@ -14,14 +14,18 @@ function OAuthError(messageOrError, properties) { var message = messageOrError instanceof Error ? messageOrError.message : messageOrError; var error = messageOrError instanceof Error ? messageOrError : null; - StandardError.call(this, message, properties, error); + if (error) { + properties.inner = error; + } + + StandardHttpError.call(this, properties.code, message, properties); } /** * Inherit prototype. */ -util.inherits(OAuthError, StandardError); +util.inherits(OAuthError, StandardHttpError); /** * Export constructor. diff --git a/package.json b/package.json index c06cd337e..44f4bc6d6 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "bluebird": "^2.9.13", "camel-case": "^1.1.1", "lodash": "^3.3.1", - "standard-error": "git+ssh://git@github.com/seegno-forks/js-standard-error#feature/add-nested-error-support", + "standard-http-error": "^1.1.0", "type-is": "^1.6.0", - "validator.js": "git+ssh://git@github.com/seegno-forks/validator.js#enhancement/add-extend-method-to-assert" + "validator.js": "^1.1.1" }, "devDependencies": { "mocha": "^2.2.1", diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index a2595d3ac..25ec3e5e3 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -918,7 +918,7 @@ describe('AuthorizeHandler integration', function() { var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); var redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client'); + url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=Bad%20Request'); }); }); From 39697d07223678fcaf53e9cf3526a2acfad8ac98 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Thu, 4 Jun 2015 14:39:19 +0100 Subject: [PATCH 24/39] Fix invalid scope error during authorization --- lib/handlers/authorize-handler.js | 26 ++++--- .../handlers/authorize-handler_test.js | 72 ++++++++++++++++++- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 32b8fd222..27e4e19c3 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -9,6 +9,7 @@ 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 OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); var Request = require('../request'); @@ -77,16 +78,22 @@ AuthorizeHandler.prototype.handle = function(request, response) { this.generateAuthorizationCode(), this.getAuthorizationCodeLifetime(), this.getClient(request), - this.getScope(request), - this.getState(request), this.getUser(request, response) ]; return Promise.all(fns) .bind(this) - .spread(function(authorizationCode, expiresAt, client, scope, state, user) { - return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, user) - .bind(this) + .spread(function(authorizationCode, expiresAt, client, user) { + var scope; + var state; + + return Promise.bind(this) + .then(function() { + scope = this.getScope(request); + state = this.getState(request); + + return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, user); + }) .then(function(code) { var responseType = this.getResponseType(request, code); var redirectUri = this.buildSuccessRedirectUri(client.redirectUri, responseType); @@ -188,7 +195,7 @@ AuthorizeHandler.prototype.getScope = function(request) { var scope = request.body.scope || request.query.scope; if (!is.nqschar(scope)) { - throw new InvalidArgumentError('Invalid parameter: `scope`'); + throw new InvalidScopeError('Invalid parameter: `scope`'); } return scope; @@ -283,12 +290,15 @@ AuthorizeHandler.prototype.buildErrorRedirectUri = function(redirectUri, error) }; /** - * Update response with the redirect uri and state parameter. + * Update response with the redirect uri and the state parameter, if available. */ AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, state) { redirectUri.query = redirectUri.query || {}; - redirectUri.query.state = state; + + if (state) { + redirectUri.query.state = state; + } response.redirect(url.format(redirectUri)); }; diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 25ec3e5e3..0455c93e3 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -10,6 +10,7 @@ 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 Promise = require('bluebird'); var Request = require('../../../lib/request'); var Response = require('../../../lib/response'); @@ -279,6 +280,75 @@ describe('AuthorizeHandler integration', function() { .catch(should.fail); }); + it('should redirect to an error response if `scope` is invalid', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + }, + getClient: function() { + return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + }, + saveAuthorizationCode: function() { + return {}; + } + }; + var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); + var request = new Request({ + body: { + client_id: 12345, + response_type: 'code' + }, + headers: { + 'Authorization': 'Bearer foo' + }, + method: {}, + query: { + scope: [], + state: 'foobar' + } + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function() { + response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60'); + }); + }); + + it('should redirect to an error response if `state` is missing', function() { + var model = { + getAccessToken: function() { + return { user: {} }; + }, + getClient: function() { + return { grants: ['authorization_code'], redirectUri: '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 return the `code` if successful', function() { var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; var model = { @@ -639,7 +709,7 @@ describe('AuthorizeHandler integration', function() { should.fail(); } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); + e.should.be.an.instanceOf(InvalidScopeError); e.message.should.equal('Invalid parameter: `scope`'); } }); From 773d1c1928c74a555256b1c89693cfb1351d3374 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 16 Jun 2015 11:47:46 +0100 Subject: [PATCH 25/39] Remove X-OAuth headers if scope is not specified --- lib/handlers/authenticate-handler.js | 4 +-- .../handlers/authenticate-handler_test.js | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 84da91275..549d2775c 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -239,11 +239,11 @@ AuthenticateHandler.prototype.validateScope = function(accessToken) { */ AuthenticateHandler.prototype.updateResponse = function(response, accessToken) { - if (this.addAcceptedScopesHeader) { + if (this.scope && this.addAcceptedScopesHeader) { response.set('X-Accepted-OAuth-Scopes', this.scope); } - if (this.addAuthorizedScopesHeader) { + if (this.scope && this.addAuthorizedScopesHeader) { response.set('X-OAuth-Scopes', accessToken.scope); } }; diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 6451ee98c..3e4c1561e 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -472,7 +472,20 @@ describe('AuthenticateHandler integration', function() { }); describe('updateResponse()', function() { - it('should set the `X-Accepted-OAuth-Scopes` header', function() { + it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function() { + var model = { + getAccessToken: function() {}, + validateScope: 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() {}, validateScope: function() {} @@ -485,6 +498,19 @@ describe('AuthenticateHandler integration', function() { 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() {}, + validateScope: 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() {}, From 0f13ecbbe175f48f343dde57a4325815ab643ff4 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 30 Jun 2015 17:55:21 +0100 Subject: [PATCH 26/39] Fix incorrect default expires_in in bearer token --- lib/handlers/token-handler.js | 2 +- lib/token-types/bearer-token-type.js | 4 ---- .../handlers/token-handler_test.js | 2 +- .../token-types/bearer-token-type_test.js | 21 +++++++++---------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 41fb4fb83..4de8a0ec0 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -215,7 +215,7 @@ TokenHandler.prototype.handleGrantType = function(request, client) { */ TokenHandler.prototype.getTokenType = function(model) { - return new BearerTokenType(model.accessToken, this.accessTokenLifetime, model.refreshToken, model.scope); + return new BearerTokenType(model.accessToken, model.accessTokenLifetime, model.refreshToken, model.scope); }; /** diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index 5d9b19d72..c900a9f62 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -14,10 +14,6 @@ function BearerTokenType(accessToken, accessTokenLifetime, refreshToken, scope) throw new InvalidArgumentError('Missing parameter: `accessToken`'); } - if (!accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - this.accessToken = accessToken; this.accessTokenLifetime = accessTokenLifetime; this.refreshToken = refreshToken; diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index e248d374c..515a63022 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -754,7 +754,7 @@ describe('TokenHandler integration', function() { var handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); var tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: 'foobar' }); - tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: 120, refreshToken: 'bar', scope: 'foobar' }); + tokenType.should.eql({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: 'foobar' }); }); }); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js index 66943ede5..276f38e1a 100644 --- a/test/integration/token-types/bearer-token-type_test.js +++ b/test/integration/token-types/bearer-token-type_test.js @@ -24,17 +24,6 @@ describe('BearerTokenType integration', function() { } }); - it('should throw an error if `accessTokenLifetime` is missing', function() { - try { - new BearerTokenType('foo'); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - it('should set the `accessToken`', function() { var responseType = new BearerTokenType('foo', 'bar'); @@ -66,6 +55,16 @@ describe('BearerTokenType integration', function() { }); }); + 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(); From c5cbe1c346d6545dd5067778d1544919eaff8a24 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Wed, 22 Jul 2015 00:24:47 +0100 Subject: [PATCH 27/39] Update travis.yml --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index df63076b8..a52f99a60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: node_js + node_js: - - "0.10" - - "0.8" + - iojs + - node + +sudo: false From eae23102479bd17c34479b8f7517a5e1e14dd93a Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Thu, 29 Oct 2015 09:26:46 +0000 Subject: [PATCH 28/39] Rename `getToken()` to `getTokenFromRequest()` --- lib/handlers/authenticate-handler.js | 4 ++-- test/integration/handlers/authenticate-handler_test.js | 6 +++--- test/unit/handlers/authenticate-handler_test.js | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 549d2775c..0bb73a29e 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -62,7 +62,7 @@ AuthenticateHandler.prototype.handle = function(request, response) { return Promise.bind(this) .then(function() { - return this.getToken(request); + return this.getTokenFromRequest(request); }) .then(function(token) { return this.getAccessToken(token); @@ -105,7 +105,7 @@ AuthenticateHandler.prototype.handle = function(request, response) { * @see https://tools.ietf.org/html/rfc6750#section-2 */ -AuthenticateHandler.prototype.getToken = function(request) { +AuthenticateHandler.prototype.getTokenFromRequest = function(request) { var headerToken = request.get('Authorization'); var queryToken = request.query.access_token; var bodyToken = request.body.access_token; diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 3e4c1561e..46189e6f2 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -194,7 +194,7 @@ describe('AuthenticateHandler integration', function() { }); }); - describe('getToken()', function() { + 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({ @@ -205,7 +205,7 @@ describe('AuthenticateHandler integration', function() { }); try { - handler.getToken(request); + handler.getTokenFromRequest(request); should.fail(); } catch (e) { @@ -219,7 +219,7 @@ describe('AuthenticateHandler integration', function() { var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { - handler.getToken(request); + handler.getTokenFromRequest(request); should.fail(); } catch (e) { diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index c7731169e..90a068365 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -13,7 +13,7 @@ var should = require('should'); */ describe('AuthenticateHandler', function() { - describe('getToken()', function() { + describe('getTokenFromRequest()', function() { describe('with bearer token in the request authorization header', function() { it('should call `getTokenFromRequestHeader()`', function() { var handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); @@ -26,7 +26,7 @@ describe('AuthenticateHandler', function() { sinon.stub(handler, 'getTokenFromRequestHeader'); - handler.getToken(request); + handler.getTokenFromRequest(request); handler.getTokenFromRequestHeader.callCount.should.equal(1); handler.getTokenFromRequestHeader.firstCall.args[0].should.equal(request); @@ -46,7 +46,7 @@ describe('AuthenticateHandler', function() { sinon.stub(handler, 'getTokenFromRequestQuery'); - handler.getToken(request); + handler.getTokenFromRequest(request); handler.getTokenFromRequestQuery.callCount.should.equal(1); handler.getTokenFromRequestQuery.firstCall.args[0].should.equal(request); @@ -66,7 +66,7 @@ describe('AuthenticateHandler', function() { sinon.stub(handler, 'getTokenFromRequestBody'); - handler.getToken(request); + handler.getTokenFromRequest(request); handler.getTokenFromRequestBody.callCount.should.equal(1); handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); From b88d33c5bcdca86045ca5b7cdc9ce551879b8164 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Thu, 29 Oct 2015 09:27:55 +0000 Subject: [PATCH 29/39] Add MIT license file --- LICENSE | 21 +++++++++++++++++++++ package.json | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..503ee5243 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 NightWorld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/package.json b/package.json index 44f4bc6d6..cdf49421e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "should": "^5.0.1", "sinon": "^1.13.0" }, - "license": "MIT", + "license": "SEE LICENSE IN LICENSE", "engines": { "node": ">=0.8" }, From 872d8c121cbb75f15ac96be6e2314874bcced93f Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Thu, 29 Oct 2015 10:15:52 +0000 Subject: [PATCH 30/39] Validate redirectUri in authorization code grant --- .../authorization-code-grant-type.js | 34 +++++++++++ .../authorization-code-grant-type_test.js | 61 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 597a0b68e..ebf863253 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -63,6 +63,9 @@ AuthorizationCodeGrantType.prototype.handle = function(request, client) { .then(function() { return this.getAuthorizationCode(request, client); }) + .tap(function(code) { + return this.validateRedirectUri(request, code); + }) .tap(function(code) { return this.revokeAuthorizationCode(code); }) @@ -110,10 +113,41 @@ AuthorizationCodeGrantType.prototype.getAuthorizationCode = function(request, cl 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. * diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 112a7ba23..95b4f61c5 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -308,6 +308,25 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); + 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' }; @@ -355,6 +374,48 @@ describe('AuthorizationCodeGrantType integration', function() { }); }); + 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 authorizationCode; }, + 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: {} }; From 5f03054de8c2ade9d629f3a1ee20313890f653a2 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 3 Nov 2015 22:26:41 +0000 Subject: [PATCH 31/39] Add redirectUri array support in authorize handler --- lib/handlers/authorize-handler.js | 17 ++++++++++---- .../handlers/authorize-handler_test.js | 22 +++++++++---------- test/integration/server_test.js | 6 ++--- test/unit/handlers/authorize-handler_test.js | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 27e4e19c3..460691486 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -84,6 +84,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { return Promise.all(fns) .bind(this) .spread(function(authorizationCode, expiresAt, client, user) { + var uri = this.getRedirectUri(request, client); var scope; var state; @@ -96,7 +97,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { }) .then(function(code) { var responseType = this.getResponseType(request, code); - var redirectUri = this.buildSuccessRedirectUri(client.redirectUri, responseType); + var redirectUri = this.buildSuccessRedirectUri(uri, responseType); this.updateResponse(response, redirectUri, state); @@ -107,7 +108,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { e = new ServerError(e); } - var redirectUri = this.buildErrorRedirectUri(client.redirectUri, e); + var redirectUri = this.buildErrorRedirectUri(uri, e); this.updateResponse(response, redirectUri, state); @@ -175,11 +176,11 @@ AuthorizeHandler.prototype.getClient = function(request) { throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); } - if (!client.redirectUri) { + if (!client.redirectUris || 0 === client.redirectUris.length) { throw new InvalidClientError('Invalid client: missing client `redirectUri`'); } - if (redirectUri && client.redirectUri !== redirectUri) { + if (redirectUri && !_.contains(client.redirectUris, redirectUri)) { throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); } @@ -229,6 +230,14 @@ AuthorizeHandler.prototype.getUser = function(request, response) { }); }; +/** + * Get redirect URI. + */ + +AuthorizeHandler.prototype.getRedirectUri = function(request, client) { + return request.body.redirect_uri || request.query.redirect_uri || client.redirectUris[0]; +}; + /** * Save authorization code. */ diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 0455c93e3..04361c97c 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -180,7 +180,7 @@ describe('AuthorizeHandler integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { throw new Error('Unhandled exception'); @@ -215,7 +215,7 @@ describe('AuthorizeHandler integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { throw new AccessDeniedError('Cannot request this auth code'); @@ -245,7 +245,7 @@ describe('AuthorizeHandler integration', function() { }); it('should redirect to a successful response with `code` and `state` if successful', function() { - var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; var model = { getAccessToken: function() { return { client: client, user: {} }; @@ -286,7 +286,7 @@ describe('AuthorizeHandler integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { return {}; @@ -322,7 +322,7 @@ describe('AuthorizeHandler integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { throw new AccessDeniedError('Cannot request this auth code'); @@ -350,7 +350,7 @@ describe('AuthorizeHandler integration', function() { }); it('should return the `code` if successful', function() { - var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; var model = { getAccessToken: function() { return { client: client, user: {} }; @@ -598,7 +598,7 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'https://example.com' }; + return { grants: ['authorization_code'], redirectUris: ['https://example.com'] }; }, saveAuthorizationCode: function() {} }; @@ -617,7 +617,7 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() { - return Promise.resolve({ grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }); + return Promise.resolve({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }); }, saveAuthorizationCode: function() {} }; @@ -636,7 +636,7 @@ describe('AuthorizeHandler integration', function() { var model = { getAccessToken: function() {}, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() {} }; @@ -653,7 +653,7 @@ describe('AuthorizeHandler integration', function() { describe('with `client_id` in the request body', function() { it('should return a client', function() { - var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; var model = { getAccessToken: function() {}, getClient: function() { @@ -674,7 +674,7 @@ describe('AuthorizeHandler integration', function() { describe('with `client_id` in the request query', function() { it('should return a client', function() { - var client = { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; var model = { getAccessToken: function() {}, getClient: function() { diff --git a/test/integration/server_test.js b/test/integration/server_test.js index bdc283f3e..a49c2369d 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -89,7 +89,7 @@ describe('Server integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { return { authorizationCode: 123 }; @@ -112,7 +112,7 @@ describe('Server integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { return { authorizationCode: 123 }; @@ -132,7 +132,7 @@ describe('Server integration', function() { return { user: {} }; }, getClient: function() { - return { grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }; + return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function() { return { authorizationCode: 123 }; diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 3e55f4734..f339587c8 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -35,7 +35,7 @@ describe('AuthorizeHandler', function() { it('should call `model.getClient()`', function() { var model = { getAccessToken: function() {}, - getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUri: 'http://example.com/cb' }), + getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }), saveAuthorizationCode: function() {} }; var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); From a9d42565b0594ea0b5a59f8643a9fe6e4c784698 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 3 Nov 2015 22:44:19 +0000 Subject: [PATCH 32/39] Fix missing redirectUri param in authorize handler --- lib/handlers/authorize-handler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 460691486..8be5388e1 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -93,7 +93,7 @@ AuthorizeHandler.prototype.handle = function(request, response) { scope = this.getScope(request); state = this.getState(request); - return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, user); + return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user); }) .then(function(code) { var responseType = this.getResponseType(request, code); @@ -242,10 +242,11 @@ AuthorizeHandler.prototype.getRedirectUri = function(request, client) { * Save authorization code. */ -AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, user) { +AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) { var code = { authorizationCode: authorizationCode, expiresAt: expiresAt, + redirectUri: redirectUri, scope: scope }; From 7f2b59b9d12a286aa912c92778ad8adbcc3b0592 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 3 Nov 2015 23:05:08 +0000 Subject: [PATCH 33/39] Add client access token and refresh token lifetime --- lib/handlers/token-handler.js | 25 +++++++++- .../handlers/token-handler_test.js | 48 +++++++++++++++++++ test/unit/handlers/authorize-handler_test.js | 6 +-- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 4de8a0ec0..a074e9b10 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -203,13 +203,36 @@ TokenHandler.prototype.handleGrantType = function(request, client) { 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: this.accessTokenLifetime, model: this.model, refreshTokenLifetime: this.refreshTokenLifetime }; + + var options = { + accessTokenLifetime: accessTokenLifetime, + model: this.model, + refreshTokenLifetime: refreshTokenLifetime + }; 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. */ diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 515a63022..04bbf5432 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -745,6 +745,54 @@ describe('TokenHandler integration', function() { }); }); + 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 = { diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index f339587c8..d03262be5 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -60,13 +60,13 @@ describe('AuthorizeHandler', function() { }; var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz') + 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', scope: 'qux' }); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: 'qux' }); model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); - model.saveAuthorizationCode.firstCall.args[2].should.equal('baz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); }) .catch(should.fail); }); From 99b86edb04e30b64f3682745f0bf4e9e1f24a4ec Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 3 Nov 2015 23:25:45 +0000 Subject: [PATCH 34/39] Add refreshToken to authorization code grant type --- lib/grant-types/authorization-code-grant-type.js | 10 ++++++++-- .../grant-types/authorization-code-grant-type_test.js | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index ebf863253..c7e953f91 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -182,12 +182,18 @@ AuthorizationCodeGrantType.prototype.revokeAuthorizationCode = function(code) { */ AuthorizationCodeGrantType.prototype.saveToken = function(user, client, authorizationCode, scope) { - return this.generateAccessToken() + const fns = [ + this.generateAccessToken(), + this.generateRefreshToken() + ]; + + return Promise.all(fns) .bind(this) - .then(function(accessToken) { + .spread(function(accessToken, refreshToken) { var token = { accessToken: accessToken, authorizationCode: authorizationCode, + refreshToken: refreshToken, scope: scope }; diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index d2dee9822..d50ac159a 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -67,12 +67,13 @@ describe('AuthorizationCodeGrantType', function() { var handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foo')); + sinon.stub(handler, 'generateRefreshToken').returns(Promise.resolve('bar')); 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', scope: 'foobiz' }); + model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', authorizationCode: 'foobar', refreshToken: 'bar', scope: 'foobiz' }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); }) From 0c50f976d5770a4a2cf12aaf45df9fe5aa934ed9 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 17 Nov 2015 22:56:17 +0000 Subject: [PATCH 35/39] Update README.md --- README.md | 357 ++++++++++-------------------------------------------- 1 file changed, 64 insertions(+), 293 deletions(-) diff --git a/README.md b/README.md index c3db39c3e..7d17a4dfb 100644 --- a/README.md +++ b/README.md @@ -1,324 +1,95 @@ -# Node OAuth2 Server [![Build Status](https://travis-ci.org/thomseddon/node-oauth2-server.png)](https://travis-ci.org/thomseddon/node-oauth2-server) +Complete, compliant and well tested module for implementing an OAuth2 server in [node.js](https://nodejs.org/). -Complete, compliant and well tested module for implementing an OAuth2 Server/Provider in [node.js](http://nodejs.org/) + [![NPM Version][npm-image]][npm-url] + [![Build Status][travis-image]][travis-url] + [![NPM Downloads][downloads-image]][downloads-url] -## Installation +# Quick Start -``` -npm install oauth2-server -``` + The _node-oauth2-server_ module is framework-agnostic but there are several wrappers available for popular frameworks such as [express](https://github.com/seegno/express-oauth-server) and [koa](https://github.com/thomseddon/koa-oauth-server). -## Quick Start - -The module provides two middlewares, one for authorization and routing, another for error handling, use them as you would any other middleware: + Using the _express_ wrapper (_recommended_): ```js -var express = require('express'), - bodyParser = require('body-parser'), - oauthserver = require('oauth2-server'); - +var express = require('express'); +var oauth = require('express-oauth-server')({ model: model }); var app = express(); -app.use(bodyParser.urlencoded({ extended: true })); - -app.use(bodyParser.json()); - -app.oauth = oauthserver({ - model: {}, // See below for specification - grants: ['password'], - debug: true -}); +app.use(oauth.authenticate()); -app.all('/oauth/token', app.oauth.grant()); - -app.get('/', app.oauth.authorise(), function (req, res) { - res.send('Secret area'); -}); - -app.use(app.oauth.errorHandler()); +app.get('/', function (req, res) { + res.send('Hello World'); +}) app.listen(3000); ``` -After running with node, visting http://127.0.0.1:3000 should present you with a json response saying your access token could not be found. - -Note: As no model was actually implemented here, delving any deeper, i.e. passing an access token, will just cause a server error. See below for the specification of what's required from the model. - -## Features - -- Supports authorization_code, password, refresh_token, client_credentials and extension (custom) grant types -- Implicitly supports any form of storage e.g. PostgreSQL, MySQL, Mongo, Redis... -- Full test suite - -## Options - -- *string* **model** - - Model object (see below) -- *array* **grants** - - grant types you wish to support, currently the module supports `authorization_code`, `password`, `refresh_token` and `client_credentials` - - Default: `[]` -- *function|boolean* **debug** - - If `true` errors will be logged to console. You may also pass a custom function, in which case that function will be called with the error as its first argument - - Default: `false` -- *number* **accessTokenLifetime** - - Life of access tokens in seconds - - If `null`, tokens will considered to never expire - - Default: `3600` -- *number* **refreshTokenLifetime** - - Life of refresh tokens in seconds - - If `null`, tokens will considered to never expire - - Default: `1209600` -- *number* **authCodeLifetime** - - Life of auth codes in seconds - - Default: `30` -- *regexp* **clientIdRegex** - - Regex to sanity check client id against before checking model. Note: the default just matches common `client_id` structures, change as needed - - Default: `/^[a-z0-9-_]{3,40}$/i` -- *boolean* **passthroughErrors** - - If true, **non grant** errors will not be handled internally (so you can ensure a consistent format with the rest of your api) -- *boolean* **continueAfterResponse** - - If true, `next` will be called even if a response has been sent (you probably don't want this) - -## Model Specification - -The module requires a model object through which some aspects or storage, retrieval and custom validation are abstracted. -The last parameter of all methods is a callback of which the first parameter is always used to indicate an error. - -Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/postgresql for a full model example using postgres. - -### Always Required - -#### getAccessToken (bearerToken, callback) -- *string* **bearerToken** - - The bearer token (access token) that has been provided -- *function* **callback (error, accessToken)** - - *mixed* **error** - - Truthy to indicate an error - - *object* **accessToken** - - The access token retrieved form storage or falsey to indicate invalid access token - - Must contain the following keys: - - *date* **expires** - - The date when it expires - - `null` to indicate the token **never expires** - - *mixed* **user** *or* *string|number* **userId** - - If a `user` key exists, this is saved as `req.user` - - Otherwise a `userId` key must exist, which is saved in `req.user.id` - -#### getClient (clientId, clientSecret, callback) -- *string* **clientId** -- *string|null* **clientSecret** - - If null, omit from search query (only search by clientId) -- *function* **callback (error, client)** - - *mixed* **error** - - Truthy to indicate an error - - *object* **client** - - The client retrieved from storage or falsey to indicate an invalid client - - Saved in `req.client` - - Must contain the following keys: - - *string* **clientId** - - *string* **redirectUri** (`authorization_code` grant type only) - -#### grantTypeAllowed (clientId, grantType, callback) -- *string* **clientId** -- *string* **grantType** -- *function* **callback (error, allowed)** - - *mixed* **error** - - Truthy to indicate an error - - *boolean* **allowed** - - Indicates whether the grantType is allowed for this clientId - -#### saveAccessToken (accessToken, clientId, expires, user, callback) -- *string* **accessToken** -- *string* **clientId** -- *date* **expires** -- *object* **user** -- *function* **callback (error)** - - *mixed* **error** - - Truthy to indicate an error - - -### Required for `authorization_code` grant type - -#### getAuthCode (authCode, callback) -- *string* **authCode** -- *function* **callback (error, authCode)** - - *mixed* **error** - - Truthy to indicate an error - - *object* **authCode** - - The authorization code retrieved form storage or falsey to indicate invalid code - - Must contain the following keys: - - *string|number* **clientId** - - client id associated with this auth code - - *date* **expires** - - The date when it expires - - *string|number* **userId** - - The userId - -#### saveAuthCode (authCode, clientId, expires, user, callback) -- *string* **authCode** -- *string* **clientId** -- *date* **expires** -- *mixed* **user** - - Whatever was passed as `user` to the codeGrant function (see example) -- *function* **callback (error)** - - *mixed* **error** - - Truthy to indicate an error - - -### Required for `password` grant type - -#### getUser (username, password, callback) -- *string* **username** -- *string* **password** -- *function* **callback (error, user)** - - *mixed* **error** - - Truthy to indicate an error - - *object* **user** - - The user retrieved from storage or falsey to indicate an invalid user - - Saved in `req.user` - - Must contain the following keys: - - *string|number* **id** - -### Required for `refresh_token` grant type - -#### saveRefreshToken (refreshToken, clientId, expires, user, callback) -- *string* **refreshToken** -- *string* **clientId** -- *date* **expires** -- *object* **user** -- *function* **callback (error)** - - *mixed* **error** - - Truthy to indicate an error - -#### getRefreshToken (refreshToken, callback) -- *string* **refreshToken** - - The bearer token (refresh token) that has been provided -- *function* **callback (error, refreshToken)** - - *mixed* **error** - - Truthy to indicate an error - - *object* **refreshToken** - - The refresh token retrieved form storage or falsey to indicate invalid refresh token - - Must contain the following keys: - - *string|number* **clientId** - - client id associated with this token - - *date* **expires** - - The date when it expires - - `null` to indicate the token **never expires** - - *string|number* **userId** - - The userId - - -### Optional for Refresh Token grant type - -#### revokeRefreshToken (refreshToken, callback) -The spec does not actually require that you revoke the old token - hence this is optional (Last paragraph: http://tools.ietf.org/html/rfc6749#section-6) -- *string* **refreshToken** -- *function* **callback (error)** - - *mixed* **error** - - Truthy to indicate an error - -### Required for [extension grant](#extension-grants) grant type - -#### extendedGrant (grantType, req, callback) -- *string* **grantType** - - The (custom) grant type -- *object* **req** - - The raw request -- *function* **callback (error, supported, user)** - - *mixed* **error** - - Truthy to indicate an error - - *boolean* **supported** - - Whether you support the grant type - - *object* **user** - - The user retrieved from storage or falsey to indicate an invalid user - - Saved in `req.user` - - Must contain the following keys: - - *string|number* **id** - -### Required for `client_credentials` grant type - -#### getUserFromClient (clientId, clientSecret, callback) -- *string* **clientId** -- *string* **clientSecret** -- *function* **callback (error, user)** - - *mixed* **error** - - Truthy to indicate an error - - *object* **user** - - The user retrieved from storage or falsey to indicate an invalid user - - Saved in `req.user` - - Must contain the following keys: - - *string|number* **id** + Using this module directly (_for custom servers only_): +```js +var Request = require('oauth2-server').Request; +var oauth = require('oauth2-server')({ model: model }); -### Optional +var request = new Request({ + headers: { authorization: 'Bearer foobar' } +}); -#### generateToken (type, req, callback) -- *string* **type** - - `accessToken` or `refreshToken` -- *object* **req** - - The current express request -- *function* **callback (error, token)** - - *mixed* **error** - - Truthy to indicate an error - - *string|object|null* **token** - - *string* indicates success - - *null* indicates to revert to the default token generator - - *object* indicates a reissue (i.e. will not be passed to saveAccessToken/saveRefreshToken) - - Must contain the following keys (if object): - - *string* **accessToken** OR **refreshToken** dependant on type +oauth.authenticate(request) + .then(function(data) { + // Request is authorized. + }) + .catch(function(e) { + // Request is not authorized. + }); +``` -## Extension Grants -You can support extension/custom grants by implementing the extendedGrant method as outlined above. -Any grant type that is a valid URI will be passed to it for you to handle (as [defined in the spec](http://tools.ietf.org/html/rfc6749#section-4.5)). -You can access the grant type via the first argument and you should pass back supported as `false` if you do not support it to ensure a consistent (and compliant) response. + _Note: see the documentation for the [specification][wiki-model-specification] of what's required from the model._ -## Example using the `password` grant type +# Features -First you must insert client id/secret and user into storage. This is out of the scope of this example. + - Supports `authorization_code` (with scopes), `client_credentials`, `password`, `refresh_token` and custom `extension` grant types. + - Can be used with _node-style_ callbacks, promises and ES6 _async_/_await_. + - Fully [RFC6749](https://tools.ietf.org/html/rfc6749) and [RFC6750](https://tools.ietf.org/html/rfc6750) compliant. + - Implicitly supports any form of storage e.g. _PostgreSQL_, _MySQL_, _Mongo_, _Redis_, _etc_. + - Full test suite. -To obtain a token you should POST to `/oauth/token`. You should include your client credentials in -the Authorization header ("Basic " + client_id:client_secret base64'd), and then grant_type ("password"), -username and password in the request body, for example: +# Documentation -``` -POST /oauth/token HTTP/1.1 -Host: server.example.com -Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW -Content-Type: application/x-www-form-urlencoded + - [Server options][wiki-server-options] + - [Model specification][wiki-model-specification] + - [Authorization Code][wiki-model-specification] + - [Client Credentials][wiki-model-specification] + - [Password][wiki-model-specification] + - [Refresh token][wiki-model-specification] + - [Custom extension][wiki-model-specification] -grant_type=password&username=johndoe&password=A3ddj3w -``` -This will then call the following on your model (in this order): - - getClient (clientId, clientSecret, callback) - - grantTypeAllowed (clientId, grantType, callback) - - getUser (username, password, callback) - - saveAccessToken (accessToken, clientId, expires, user, callback) - - saveRefreshToken (refreshToken, clientId, expires, user, callback) **(if using)** +# Examples -Provided there weren't any errors, this will return the following (excluding the `refresh_token` if you've not enabled the refresh_token grant type): + Most users should refer to our [express](https://github.com/seegno/express-oauth-server/tree/master/examples) or [koa](https://github.com/thomseddon/koa-oauth-server/tree/master/examples) examples. If you're implementing a custom server, we have many examples available: -``` -HTTP/1.1 200 OK -Content-Type: application/json;charset=UTF-8 -Cache-Control: no-store -Pragma: no-cache + - A simple **password** grant authorization [example](examples/password). + - A more complex **password** and **refresh_token** [example](examples/refresh-token). + - An advanced **password**, **refresh_token** and **authorization_code** (with scopes) [example](examples/authorization-code). -{ - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"bearer", - "expires_in":3600, - "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" -} -``` +# Upgrading from 2.x -## Changelog + This module has been rewritten with a promise-based approach and introduced a few changes in the model specification. -See: https://github.com/thomseddon/node-oauth2-server/blob/master/Changelog.md + Please refer to our [3.0 migration guide][wiki-migrating-from-2x-to-3x] for more information. -## Credits +## License -Copyright (c) 2013 Thom Seddon + [MIT](LICENSE) -## License + +[npm-image]: https://img.shields.io/npm/v/node-oauth2-server.svg +[npm-url]: https://npmjs.org/package/node-oauth2-server +[travis-image]: https://img.shields.io/travis/thomseddon/node-oauth2-server/master.svg +[travis-url]: https://travis-ci.org/thomseddon/node-oauth2-server +[downloads-image]: https://img.shields.io/npm/dm/node-oauth2-server.svg +[downloads-url]: https://npmjs.org/package/node-oauth2-server -[Apache, Version 2.0](https://github.com/thomseddon/node-oauth2-server/blob/master/LICENSE) + +[wiki-model-specification]: https://github.com/thomseddon/node-oauth2-server/wiki/Model-specification +[wiki-migrating-from-2x-to-3x]: https://github.com/thomseddon/node-oauth2-server/wiki/Migrating-from-2-x-to-3-x +[wiki-server-options]: https://github.com/thomseddon/node-oauth2-server/wiki/Server-options From 85a0097cd25935a0938d813df900cb771d6cfebf Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Mon, 4 Jan 2016 23:58:10 +0000 Subject: [PATCH 36/39] Add `allowBearerTokensInQueryString` option --- lib/handlers/authenticate-handler.js | 9 +++++++-- lib/server.js | 3 ++- test/integration/handlers/authenticate-handler_test.js | 6 ++++++ test/integration/server_test.js | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 0bb73a29e..5c0f90121 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -43,6 +43,7 @@ function AuthenticateHandler(options) { this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; + this.allowBearerTokensInQueryString = options.allowBearerTokensInQueryString; this.model = options.model; this.scope = options.scope; } @@ -160,8 +161,12 @@ AuthenticateHandler.prototype.getTokenFromRequestHeader = function(request) { * @see http://tools.ietf.org/html/rfc6750#section-2.3 */ -AuthenticateHandler.prototype.getTokenFromRequestQuery = function() { - throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); +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; }; /** diff --git a/lib/server.js b/lib/server.js index 045be77ef..24660a101 100644 --- a/lib/server.js +++ b/lib/server.js @@ -30,7 +30,8 @@ function OAuth2Server(options) { OAuth2Server.prototype.authenticate = function(request, response, options, callback) { options = _.assign({ addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true + addAuthorizedScopesHeader: true, + allowBearerTokensInQueryString: false }, this.options, options); return new AuthenticateHandler(options) diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 46189e6f2..e7828d0f4 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -281,6 +281,12 @@ describe('AuthenticateHandler integration', function() { 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() { diff --git a/test/integration/server_test.js b/test/integration/server_test.js index a49c2369d..0c1c430d8 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -50,6 +50,7 @@ describe('Server integration', function() { .then(function() { this.addAcceptedScopesHeader.should.be.true; this.addAuthorizedScopesHeader.should.be.true; + this.allowBearerTokensInQueryString.should.be.false; }) .catch(should.fail); }); From db73196087b232f35c0d2c525f4e35668e83457f Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Sun, 10 Jan 2016 16:44:50 +0000 Subject: [PATCH 37/39] Remove refresh token from client credentials grant Fixes #174 --- lib/grant-types/client-credentials-grant-type.js | 8 ++------ .../grant-types/client-credentials-grant-type_test.js | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index c43ff36be..b4cbe8b3f 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -85,19 +85,15 @@ ClientCredentialsGrantType.prototype.getUserFromClient = function(client) { ClientCredentialsGrantType.prototype.saveToken = function(user, client, scope) { var fns = [ this.generateAccessToken(), - this.generateRefreshToken(), - this.getAccessTokenExpiresAt(), - this.getRefreshTokenExpiresAt() + this.getAccessTokenExpiresAt() ]; return Promise.all(fns) .bind(this) - .spread(function(accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt) { + .spread(function(accessToken, accessTokenExpiresAt) { var token = { accessToken: accessToken, accessTokenExpiresAt: accessTokenExpiresAt, - refreshToken: refreshToken, - refreshTokenExpiresAt: refreshTokenExpiresAt, scope: scope }; diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index c68b46ac4..d83e3747c 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -42,15 +42,13 @@ describe('ClientCredentialsGrantType', function() { var handler = new ClientCredentialsGrantType({ 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[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); }) From 4b7edccdb718b784aec4b6264b630de00efd9c79 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Sun, 10 Jan 2016 18:08:22 +0000 Subject: [PATCH 38/39] Add custom authentication in authorization handler --- lib/handlers/authorize-handler.js | 18 ++++++++++++--- .../handlers/authorize-handler_test.js | 19 +++++++++++++++ test/unit/handlers/authorize-handler_test.js | 23 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 8be5388e1..316257255 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -36,6 +36,10 @@ var responseTypes = { function AuthorizeHandler(options) { options = options || {}; + if (options.authenticateHandler && !options.authenticateHandler.handle) { + throw new InvalidArgumentError('Invalid argument: authenticateHandler does not implement `handle()`'); + } + if (!options.authorizationCodeLifetime) { throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); } @@ -53,7 +57,7 @@ function AuthorizeHandler(options) { } this.authorizationCodeLifetime = options.authorizationCodeLifetime; - this.authenticateHandler = new AuthenticateHandler(options); + this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); this.model = options.model; } @@ -225,8 +229,16 @@ AuthorizeHandler.prototype.getState = function(request) { */ AuthorizeHandler.prototype.getUser = function(request, response) { - return this.authenticateHandler.handle(request, response).then(function(token) { - return token.user; + if (this.authenticateHandler instanceof AuthenticateHandler) { + return this.authenticateHandler.handle(request, response).get('user'); + } + + return Promise.try(this.authenticateHandler.handle, [request, response]).then(function(user) { + if (!user) { + throw new ServerError('Server error: `handle()` did not return a `user` object'); + } + + return user; }); }; diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 04361c97c..a23ed4b93 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -14,6 +14,7 @@ var InvalidScopeError = require('../../../lib/errors/invalid-scope-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'); @@ -812,6 +813,24 @@ describe('AuthorizeHandler integration', function() { }); 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 = { diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index d03262be5..bb8c3d955 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -5,6 +5,7 @@ var AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); var Request = require('../../../lib/request'); +var Response = require('../../../lib/response'); var sinon = require('sinon'); var should = require('should'); @@ -51,6 +52,28 @@ describe('AuthorizeHandler', function() { }); }); + describe('getUser()', function() { + it('should call `authenticateHandler.getUser()`', function() { + var authenticateHandler = { handle: sinon.stub().returns(Promise.resolve({})) }; + var model = { + getClient: function() {}, + saveAuthorizationCode: function() {} + }; + var handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + var response = new Response(); + + return handler.getUser(request, response) + .then(function() { + authenticateHandler.handle.callCount.should.equal(1); + authenticateHandler.handle.firstCall.args.should.have.length(2); + authenticateHandler.handle.firstCall.args[0].should.equal(request); + authenticateHandler.handle.firstCall.args[1].should.equal(response); + }) + .catch(should.fail); + }); + }); + describe('saveAuthorizationCode()', function() { it('should call `model.saveAuthorizationCode()`', function() { var model = { From 82973a5c75cfa60bbd02049e3ba48080b8ccd240 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Thu, 4 Feb 2016 23:47:19 +0000 Subject: [PATCH 39/39] Add optional state to authorization handler --- lib/handlers/authorize-handler.js | 5 +++-- lib/server.js | 1 + test/integration/handlers/authorize-handler_test.js | 4 ++-- test/integration/server_test.js | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 316257255..832ced4ae 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -56,8 +56,9 @@ function AuthorizeHandler(options) { throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); } - this.authorizationCodeLifetime = options.authorizationCodeLifetime; + this.allowEmptyState = options.allowEmptyState; this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); + this.authorizationCodeLifetime = options.authorizationCodeLifetime; this.model = options.model; } @@ -213,7 +214,7 @@ AuthorizeHandler.prototype.getScope = function(request) { AuthorizeHandler.prototype.getState = function(request) { var state = request.body.state || request.query.state; - if (!state) { + if (!this.allowEmptyState && !state) { throw new InvalidRequestError('Missing parameter: `state`'); } diff --git a/lib/server.js b/lib/server.js index 24660a101..909b4287e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -45,6 +45,7 @@ OAuth2Server.prototype.authenticate = function(request, response, options, callb OAuth2Server.prototype.authorize = function(request, response, options, callback) { options = _.assign({ + allowEmptyState: false, authorizationCodeLifetime: 5 * 60 // 5 minutes. }, this.options, options); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index a23ed4b93..618abf87a 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -745,13 +745,13 @@ describe('AuthorizeHandler integration', function() { }); describe('getState()', function() { - it('should throw an error if `state` is missing', 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({ authorizationCodeLifetime: 120, model: model }); + var handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); try { diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 0c1c430d8..567e4da0b 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -102,6 +102,7 @@ describe('Server integration', function() { return server.authorize(request, response) .then(function() { + this.allowEmptyState.should.be.false; this.authorizationCodeLifetime.should.equal(300); }) .catch(should.fail);