├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── lib ├── errors │ ├── authorizationerror.js │ └── oauth2error.js ├── grant │ ├── codeIdToken.js │ ├── codeIdTokenToken.js │ ├── codeToken.js │ ├── idToken.js │ └── idTokenToken.js ├── index.js ├── request │ └── extensions.js └── response │ └── fragment.js ├── package-lock.json ├── package.json └── test ├── bootstrap └── node.js ├── grant ├── codeIdToken.test.js ├── codeIdTokenToken.test.js ├── codeToken.test.js ├── idToken.test.js └── idTokenToken.test.js ├── package.test.js └── request └── extensions.test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jaredhanson 2 | patreon: jaredhanson 3 | ko_fi: jaredhanson 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ** READ THIS FIRST! ** 2 | 3 | #### Are you looking for help? 4 | 5 | Reminder: The issue tracker is not a support forum. 6 | 7 | Issues should only be filed in this project once they are able to be reproduced 8 | and confirmed as a flaw in the software or incorrect information in associated 9 | documention. 10 | 11 | If you are encountering problems integrating this module into your application, 12 | please post a question on the [discussion forum](https://github.com/jaredhanson/oauth2orize-discuss) 13 | rather than filing an issue. 14 | 15 | #### Is this a security issue? 16 | 17 | Do not open issues that might have security implications. Potential security 18 | vulnerabilities should be reported privately to jaredhanson@gmail.com. Once any 19 | vulerabilities have been repaired, the details will be disclosed publicly in a 20 | responsible manner. This also allows time for coordinating with affected parties 21 | in order to mitigate negative consequences. 22 | 23 | 24 | If neither of the above two scenarios apply to your situation, you should open 25 | an issue. Delete this paragraph and the text above, and fill in the information 26 | requested below. 27 | 28 | 29 | 30 | 31 | 32 | 33 | ### Expected behavior 34 | 35 | 36 | 37 | ### Actual behavior 38 | 39 | 40 | 41 | ### Steps to reproduce 42 | 43 | 44 | 45 | ```js 46 | // Format code using Markdown code blocks 47 | ``` 48 | 49 | ### Environment 50 | 51 | * Operating System: 52 | * Node version: 53 | * oauth2orize version: 54 | * oauth2orize-openid version: 55 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ** READ THIS FIRST! ** 2 | 3 | #### Are you implementing a new feature? 4 | 5 | Requests for new features should first be discussed on the [developer forum](https://github.com/jaredhanson/oauth2orize-develop). 6 | This allows the community to gather feedback and assess whether or not there is 7 | an existing way to achieve the desired functionality. 8 | 9 | If it is determined that a new feature needs to be implemented, include a link 10 | to the relevant discussion along with the pull request. 11 | 12 | #### Is this a security patch? 13 | 14 | Do not open pull requests that might have security implications. Potential 15 | security vulnerabilities should be reported privately to jaredhanson@gmail.com. 16 | Once any vulerabilities have been repaired, the details will be disclosed 17 | publicly in a responsible manner. This also allows time for coordinating with 18 | affected parties in order to mitigate negative consequences. 19 | 20 | 21 | If neither of the above two scenarios apply to your situation, you should open 22 | a pull request. Delete this paragraph and the text above, and fill in the 23 | information requested below. 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ### Checklist 33 | 34 | 35 | 36 | 37 | - [ ] I have read the [CONTRIBUTING](https://github.com/jaredhanson/oauth2orize-openid/blob/master/CONTRIBUTING.md) guidelines. 38 | - [ ] I have added test cases which verify the correct operation of this feature or patch. 39 | - [ ] I have added documentation pertaining to this feature or patch. 40 | - [ ] The automated test suite (`$ make test`) executes successfully. 41 | - [ ] The automated code linting (`$ make lint`) executes successfully. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | reports/ 3 | 4 | # Mac OS X 5 | .DS_Store 6 | 7 | # Node.js 8 | node_modules 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "forin": true, 7 | "immed": true, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "noempty": true, 12 | "nonew": true, 13 | "quotmark": "single", 14 | "undef": true, 15 | "unused": true, 16 | "trailing": true, 17 | "laxcomma": true 18 | } 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | docs/ 3 | examples/ 4 | reports/ 5 | test/ 6 | 7 | .jshintrc 8 | .travis.yml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | - "3" # io.js 7 | - "2" # io.js 8 | - "1" # io.js 9 | - "0.12" 10 | - "0.10" 11 | - "0.8" 12 | - "0.6" 13 | 14 | 15 | before_install: 16 | - "npm install make-node@0.3.x -g" 17 | - "preinstall-compat" 18 | 19 | script: 20 | - "make test-cov" 21 | 22 | after_success: 23 | - "make report-cov" 24 | 25 | sudo: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.4.1] - 2021-11-29 10 | 11 | ## [0.4.0] - 2017-03-07 12 | 13 | ## [0.3.0] - 2017-02-15 14 | 15 | ## [0.2.1] - 2016-12-28 16 | 17 | ## [0.2.0] - 2016-11-17 18 | 19 | ## [0.1.3] - 2016-10-21 20 | 21 | ## [0.1.2] - 2016-10-04 22 | 23 | ## [0.1.1] - 2016-07-22 24 | 25 | ## [0.1.0] - 2016-07-19 26 | 27 | ## [0.0.1] - 2015-09-23 28 | 29 | - Initial release. 30 | 31 | [Unreleased]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.4.1...HEAD 32 | [0.4.1]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.4.0...v0.4.1 33 | [0.4.0]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.3.0...v0.4.0 34 | [0.3.0]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.2.1...v0.3.0 35 | [0.2.1]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.2.0...v0.2.1 36 | [0.2.0]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.1.3...v0.2.0 37 | [0.1.3]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.1.2...v0.1.3 38 | [0.1.2]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.1.1...v0.1.2 39 | [0.1.1]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.1.0...v0.1.1 40 | [0.1.0]: https://github.com/jaredhanson/oauth2orize-openid/compare/v0.0.1...v0.1.0 41 | [0.0.1]: https://github.com/jaredhanson/oauth2orize-openid/releases/tag/v0.0.1 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | ### Tests 4 | 5 | The test suite is located in the `test/` directory. All new features are 6 | expected to have corresponding test cases with complete code coverage. Patches 7 | that increase test coverage are happily accepted. 8 | 9 | Ensure that the test suite passes by executing: 10 | 11 | ```bash 12 | $ make test 13 | ``` 14 | 15 | Coverage reports can be viewed by executing: 16 | 17 | ```bash 18 | $ make test-cov 19 | $ make view-cov 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Jared Hanson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include node_modules/make-node/main.mk 2 | 3 | 4 | SOURCES = lib/*.js lib/**/*.js 5 | TESTS = test/*.test.js test/**/*.test.js 6 | 7 | LCOVFILE = ./reports/coverage/lcov.info 8 | 9 | MOCHAFLAGS = --require ./test/bootstrap/node 10 | 11 | 12 | view-docs: 13 | open ./docs/index.html 14 | 15 | view-cov: 16 | open ./reports/coverage/lcov-report/index.html 17 | 18 | clean: clean-docs clean-cov 19 | -rm -r $(REPORTSDIR) 20 | 21 | clobber: clean 22 | -rm -r node_modules 23 | 24 | 25 | .PHONY: clean clobber 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oauth2orize-openid 2 | 3 | [OAuth2orize](https://github.com/jaredhanson/oauth2orize) extensions providing 4 | support for [OpenID Connect](https://openid.net/connect/). 5 | 6 | ## Examples 7 | 8 | This [example](https://github.com/gerges-beshay/oauth2orize-openid-examples) demonstrates 9 | basic usage of the api. 10 | 11 | ## License 12 | 13 | [The MIT License](http://opensource.org/licenses/MIT) 14 | 15 | Copyright (c) 2013 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)> 16 | -------------------------------------------------------------------------------- /lib/errors/authorizationerror.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var OAuth2Error = require('./oauth2error'); 5 | 6 | /** 7 | * `AuthorizationError` error. 8 | * 9 | * @api public 10 | */ 11 | function AuthorizationError(message, code, uri, status) { 12 | if (!status) { 13 | switch (code) { 14 | case 'invalid_request': status = 400; break; 15 | case 'unauthorized_client': status = 403; break; 16 | case 'access_denied': status = 403; break; 17 | case 'unsupported_response_type': status = 501; break; 18 | case 'invalid_scope': status = 400; break; 19 | case 'temporarily_unavailable': status = 503; break; 20 | } 21 | } 22 | 23 | OAuth2Error.call(this, message, code, uri, status); 24 | Error.captureStackTrace(this, arguments.callee); 25 | this.name = 'AuthorizationError'; 26 | } 27 | 28 | /** 29 | * Inherit from `OAuth2Error`. 30 | */ 31 | AuthorizationError.prototype.__proto__ = OAuth2Error.prototype; 32 | 33 | 34 | /** 35 | * Expose `AuthorizationError`. 36 | */ 37 | module.exports = AuthorizationError; 38 | -------------------------------------------------------------------------------- /lib/errors/oauth2error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `OAuth2Error` error. 3 | * 4 | * @api public 5 | */ 6 | function OAuth2Error(message, code, uri, status) { 7 | Error.call(this); 8 | this.message = message; 9 | this.code = code || 'server_error'; 10 | this.uri = uri; 11 | this.status = status || 500; 12 | } 13 | 14 | /** 15 | * Inherit from `Error`. 16 | */ 17 | OAuth2Error.prototype.__proto__ = Error.prototype; 18 | 19 | 20 | /** 21 | * Expose `OAuth2Error`. 22 | */ 23 | module.exports = OAuth2Error; 24 | -------------------------------------------------------------------------------- /lib/grant/codeIdToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var AuthorizationError = require('../errors/authorizationerror'); 5 | 6 | 7 | /** 8 | * Handles requests to obtain a response with an authorization code and ID 9 | * token. 10 | * 11 | * References: 12 | * - [OpenID Connect Standard 1.0 - draft 21](http://openid.net/specs/openid-connect-standard-1_0.html) 13 | * - [OpenID Connect Messages 1.0 - draft 20](http://openid.net/specs/openid-connect-messages-1_0.html) 14 | * - [OAuth 2.0 Multiple Response Type Encoding Practices - draft 08](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) 15 | * 16 | * @param {Object} options 17 | * @param {Function} issue 18 | * @return {Object} module 19 | * @api public 20 | */ 21 | module.exports = function(options, issueCode, issueIDToken) { 22 | if (typeof options == 'function') { 23 | issueIDToken = issueCode; 24 | issueCode = options; 25 | options = undefined; 26 | } 27 | options = options || {}; 28 | 29 | if (!issueCode) throw new TypeError('oauth2orize-openid.codeIDToken grant requires an issueCode callback'); 30 | if (!issueIDToken) throw new TypeError('oauth2orize-openid.codeIDToken grant requires an issueIDToken callback'); 31 | 32 | var modes = options.modes || {}; 33 | if (!modes.fragment) { 34 | modes.fragment = require('../response/fragment'); 35 | } 36 | 37 | // For maximum flexibility, multiple scope spearators can optionally be 38 | // allowed. This allows the server to accept clients that separate scope 39 | // with either space or comma (' ', ','). This violates the specification, 40 | // but achieves compatibility with existing client libraries that are already 41 | // deployed. 42 | var separators = options.scopeSeparator || ' '; 43 | if (!Array.isArray(separators)) { 44 | separators = [ separators ]; 45 | } 46 | 47 | 48 | /* Parse requests that request `code id_token` as `response_type`. 49 | * 50 | * @param {http.ServerRequest} req 51 | * @api public 52 | */ 53 | function request(req) { 54 | var clientID = req.query['client_id'] 55 | , redirectURI = req.query['redirect_uri'] 56 | , scope = req.query['scope'] 57 | , state = req.query['state'] 58 | , nonce = req.query['nonce']; 59 | 60 | if (!clientID) { throw new AuthorizationError('Missing required parameter: client_id', 'invalid_request'); } 61 | if (typeof clientID !== 'string') { throw new AuthorizationError('Invalid parameter: client_id must be a string', 'invalid_request'); } 62 | 63 | if (!nonce) { throw new AuthorizationError('Missing required parameter: nonce', 'invalid_request'); } 64 | if (typeof nonce !== 'string') { throw new AuthorizationError('Invalid parameter: nonce must be a string', 'invalid_request'); } 65 | 66 | if (scope) { 67 | if (typeof scope !== 'string') { 68 | throw new AuthorizationError('Invalid parameter: scope must be a string', 'invalid_request'); 69 | } 70 | 71 | for (var i = 0, len = separators.length; i < len; i++) { 72 | var separated = scope.split(separators[i]); 73 | // only separate on the first matching separator. this allows for a sort 74 | // of separator "priority" (ie, favor spaces then fallback to commas) 75 | if (separated.length > 1) { 76 | scope = separated; 77 | break; 78 | } 79 | } 80 | 81 | if (!Array.isArray(scope)) { scope = [ scope ]; } 82 | } 83 | 84 | return { 85 | clientID: clientID, 86 | redirectURI: redirectURI, 87 | scope: scope, 88 | state: state, 89 | nonce: nonce 90 | } 91 | } 92 | 93 | /* Sends responses to transactions that request `code id_token` as `response_type`. 94 | * 95 | * @param {Object} txn 96 | * @param {http.ServerResponse} res 97 | * @param {Function} complete 98 | * @param {Function} next 99 | * @api public 100 | */ 101 | function response(txn, res, complete, next) { 102 | if (next === undefined) { 103 | next = complete; 104 | complete = function(cb) { return cb(); }; 105 | } 106 | 107 | var mode = 'fragment' 108 | , respond; 109 | if (txn.req && txn.req.responseMode) { 110 | mode = txn.req.responseMode; 111 | } 112 | respond = modes[mode]; 113 | 114 | if (!respond) { 115 | // http://lists.openid.net/pipermail/openid-specs-ab/Week-of-Mon-20140317/004680.html 116 | return next(new AuthorizationError('Unsupported response mode: ' + mode, 'unsupported_response_mode', null, 501)); 117 | } 118 | if (respond && respond.validate) { 119 | try { 120 | respond.validate(txn); 121 | } catch(ex) { 122 | return next(ex); 123 | } 124 | } 125 | 126 | if (!txn.res.allow) { 127 | var params = { error: 'access_denied' }; 128 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 129 | return respond(txn, res, params); 130 | } 131 | 132 | function doIssueIDToken(tok) { 133 | function issued(err, idToken) { 134 | if (err) { return next(err); } 135 | if (!idToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 136 | 137 | tok['id_token'] = idToken; 138 | if (txn.req && txn.req.state) { tok['state'] = txn.req.state; } 139 | 140 | complete(function(err) { 141 | if (err) { return next(err); } 142 | return respond(txn, res, tok); 143 | }); 144 | } 145 | 146 | try { 147 | // NOTE: To facilitate code reuse, the `issueIDToken` callback should 148 | // interoperate with the `issue` callback implemented by 149 | // `oauth2orize-openid.grant.idToken`. 150 | 151 | var arity = issueIDToken.length; 152 | if (arity == 7) { 153 | issueIDToken(txn.client, txn.user, txn.res, txn.req, { authorizationCode: tok.code }, txn.locals, issued); 154 | } else if (arity == 6) { 155 | issueIDToken(txn.client, txn.user, txn.res, txn.req, { authorizationCode: tok.code }, issued); 156 | } else if (arity == 5) { 157 | issueIDToken(txn.client, txn.user, txn.res, txn.req, issued); 158 | } else { // arity == 4 159 | issueIDToken(txn.client, txn.user, txn.req, issued); 160 | } 161 | } catch (ex) { 162 | return next(ex); 163 | } 164 | } 165 | 166 | function doIssueCode() { 167 | function issued(err, code) { 168 | if (err) { return next(err); } 169 | if (!code) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 170 | 171 | var tok = {}; 172 | tok['code'] = code; 173 | 174 | doIssueIDToken(tok); 175 | } 176 | 177 | try { 178 | // NOTE: To facilitate code reuse, the `issueCode` callback should 179 | // interoperate with the `issue` callback implemented by 180 | // `oauth2orize.grant.code`. 181 | 182 | var arity = issueCode.length; 183 | if (arity == 7) { 184 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, txn.locals, issued); 185 | } else if (arity == 6) { 186 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, issued); 187 | } else if (arity == 5) { 188 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, issued); 189 | } else { // arity == 4 190 | issueCode(txn.client, txn.req.redirectURI, txn.user, issued); 191 | } 192 | } catch (ex) { 193 | return next(ex); 194 | } 195 | } 196 | 197 | doIssueCode(); 198 | } 199 | 200 | function errorHandler(err, txn, res, next) { 201 | var mode = 'fragment' 202 | , params = {} 203 | , respond; 204 | if (txn.req && txn.req.responseMode) { 205 | mode = txn.req.responseMode; 206 | } 207 | respond = modes[mode]; 208 | 209 | if (!respond) { 210 | return next(err); 211 | } 212 | if (respond && respond.validate) { 213 | try { 214 | respond.validate(txn); 215 | } catch(ex) { 216 | return next(err); 217 | } 218 | } 219 | 220 | params.error = err.code || 'server_error'; 221 | if (err.message) { params.error_description = err.message; } 222 | if (err.uri) { params.error_uri = err.uri; } 223 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 224 | return respond(txn, res, params); 225 | } 226 | 227 | /** 228 | * Return `code id_token` grant module. 229 | */ 230 | var mod = {}; 231 | mod.name = 'code id_token'; 232 | mod.request = request; 233 | mod.response = response; 234 | mod.error = errorHandler; 235 | return mod; 236 | } 237 | -------------------------------------------------------------------------------- /lib/grant/codeIdTokenToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var merge = require('utils-merge') 5 | , AuthorizationError = require('../errors/authorizationerror'); 6 | 7 | 8 | /** 9 | * Handles requests to obtain a response with an access token, authorization 10 | * code, and ID token. 11 | * 12 | * References: 13 | * - [OpenID Connect Standard 1.0 - draft 21](http://openid.net/specs/openid-connect-standard-1_0.html) 14 | * - [OpenID Connect Messages 1.0 - draft 20](http://openid.net/specs/openid-connect-messages-1_0.html) 15 | * - [OAuth 2.0 Multiple Response Type Encoding Practices - draft 08](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) 16 | * 17 | * @param {Object} options 18 | * @param {Function} issue 19 | * @return {Object} module 20 | * @api public 21 | */ 22 | module.exports = function(options, issueToken, issueCode, issueIDToken) { 23 | if (typeof options == 'function') { 24 | issueIDToken = issueCode; 25 | issueCode = issueToken; 26 | issueToken = options; 27 | options = undefined; 28 | } 29 | options = options || {}; 30 | 31 | if (!issueToken) throw new TypeError('oauth2orize-openid.codeIDTokenToken grant requires an issueToken callback'); 32 | if (!issueCode) throw new TypeError('oauth2orize-openid.codeIDTokenToken grant requires an issueCode callback'); 33 | if (!issueIDToken) throw new TypeError('oauth2orize-openid.codeIDTokenToken grant requires an issueIDToken callback'); 34 | 35 | var modes = options.modes || {}; 36 | if (!modes.fragment) { 37 | modes.fragment = require('../response/fragment'); 38 | } 39 | 40 | // For maximum flexibility, multiple scope spearators can optionally be 41 | // allowed. This allows the server to accept clients that separate scope 42 | // with either space or comma (' ', ','). This violates the specification, 43 | // but achieves compatibility with existing client libraries that are already 44 | // deployed. 45 | var separators = options.scopeSeparator || ' '; 46 | if (!Array.isArray(separators)) { 47 | separators = [ separators ]; 48 | } 49 | 50 | 51 | /* Parse requests that request `code id_token token` as `response_type`. 52 | * 53 | * @param {http.ServerRequest} req 54 | * @api public 55 | */ 56 | function request(req) { 57 | var clientID = req.query['client_id'] 58 | , redirectURI = req.query['redirect_uri'] 59 | , scope = req.query['scope'] 60 | , state = req.query['state'] 61 | , nonce = req.query['nonce']; 62 | 63 | if (!clientID) { throw new AuthorizationError('Missing required parameter: client_id', 'invalid_request'); } 64 | if (typeof clientID !== 'string') { throw new AuthorizationError('Invalid parameter: client_id must be a string', 'invalid_request'); } 65 | 66 | if (!nonce) { throw new AuthorizationError('Missing required parameter: nonce', 'invalid_request'); } 67 | if (typeof nonce !== 'string') { throw new AuthorizationError('Invalid parameter: nonce must be a string', 'invalid_request'); } 68 | 69 | if (scope) { 70 | if (typeof scope !== 'string') { 71 | throw new AuthorizationError('Invalid parameter: scope must be a string', 'invalid_request'); 72 | } 73 | 74 | for (var i = 0, len = separators.length; i < len; i++) { 75 | var separated = scope.split(separators[i]); 76 | // only separate on the first matching separator. this allows for a sort 77 | // of separator "priority" (ie, favor spaces then fallback to commas) 78 | if (separated.length > 1) { 79 | scope = separated; 80 | break; 81 | } 82 | } 83 | 84 | if (!Array.isArray(scope)) { scope = [ scope ]; } 85 | } 86 | 87 | return { 88 | clientID: clientID, 89 | redirectURI: redirectURI, 90 | scope: scope, 91 | state: state, 92 | nonce: nonce 93 | } 94 | } 95 | 96 | /* Sends responses to transactions that request `code id_token token` as `response_type`. 97 | * 98 | * @param {Object} txn 99 | * @param {http.ServerResponse} res 100 | * @param {Function} complete 101 | * @param {Function} next 102 | * @api public 103 | */ 104 | function response(txn, res, complete, next) { 105 | if (next === undefined) { 106 | next = complete; 107 | complete = function(cb) { return cb(); }; 108 | } 109 | 110 | var mode = 'fragment' 111 | , respond; 112 | if (txn.req && txn.req.responseMode) { 113 | mode = txn.req.responseMode; 114 | } 115 | respond = modes[mode]; 116 | 117 | if (!respond) { 118 | // http://lists.openid.net/pipermail/openid-specs-ab/Week-of-Mon-20140317/004680.html 119 | return next(new AuthorizationError('Unsupported response mode: ' + mode, 'unsupported_response_mode', null, 501)); 120 | } 121 | if (respond && respond.validate) { 122 | try { 123 | respond.validate(txn); 124 | } catch(ex) { 125 | return next(ex); 126 | } 127 | } 128 | 129 | if (!txn.res.allow) { 130 | var params = { error: 'access_denied' }; 131 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 132 | return respond(txn, res, params); 133 | } 134 | 135 | function doIssueIDToken(tok) { 136 | function issued(err, idToken) { 137 | if (err) { return next(err); } 138 | if (!idToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 139 | 140 | tok['id_token'] = idToken; 141 | 142 | complete(function(err) { 143 | if (err) { return next(err); } 144 | return respond(txn, res, tok); 145 | }); 146 | } 147 | 148 | try { 149 | // NOTE: To facilitate code reuse, the `issueIDToken` callback should 150 | // interoperate with the `issue` callback implemented by 151 | // `oauth2orize-openid.grant.idToken`. 152 | 153 | var arity = issueIDToken.length; 154 | if (arity == 7) { 155 | issueIDToken(txn.client, txn.user, txn.res, txn.req, { accessToken: tok.access_token, authorizationCode: tok.code }, txn.locals, issued); 156 | } else if (arity == 6) { 157 | issueIDToken(txn.client, txn.user, txn.res, txn.req, { accessToken: tok.access_token, authorizationCode: tok.code }, issued); 158 | } else if (arity == 5) { 159 | issueIDToken(txn.client, txn.user, txn.res, txn.req, issued); 160 | } else { // arity == 4 161 | issueIDToken(txn.client, txn.user, txn.req, issued); 162 | } 163 | } catch (ex) { 164 | return next(ex); 165 | } 166 | } 167 | 168 | function doIssueCode(tok) { 169 | function issued(err, code) { 170 | if (err) { return next(err); } 171 | if (!code) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 172 | 173 | tok['code'] = code; 174 | 175 | doIssueIDToken(tok); 176 | } 177 | 178 | try { 179 | // NOTE: To facilitate code reuse, the `issueCode` callback should 180 | // interoperate with the `issue` callback implemented by 181 | // `oauth2orize.grant.code`. 182 | 183 | var arity = issueCode.length; 184 | if (arity == 7) { 185 | var locals = txn.locals || {}; 186 | locals.accessToken = tok.access_token; 187 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, txn.locals, issued); 188 | } else if (arity == 6) { 189 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, issued); 190 | } else if (arity == 5) { 191 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, issued); 192 | } else { // arity == 4 193 | issueCode(txn.client, txn.req.redirectURI, txn.user, issued); 194 | } 195 | } catch (ex) { 196 | return next(ex); 197 | } 198 | } 199 | 200 | function doIssueToken() { 201 | function issued(err, accessToken, params) { 202 | if (err) { return next(err); } 203 | if (!accessToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 204 | 205 | var tok = {}; 206 | tok['access_token'] = accessToken; 207 | if (params) { merge(tok, params); } 208 | tok['token_type'] = tok['token_type'] || 'Bearer'; 209 | if (txn.req && txn.req.state) { tok['state'] = txn.req.state; } 210 | 211 | doIssueCode(tok); 212 | } 213 | 214 | try { 215 | // NOTE: To facilitate code reuse, the `issueToken` callback should 216 | // interoperate with the `issue` callback implemented by 217 | // `oauth2orize.grant.token`. 218 | 219 | var arity = issueToken.length; 220 | if (arity == 6) { 221 | issueToken(txn.client, txn.user, txn.res, txn.req, txn.locals, issued); 222 | } else if (arity == 5) { 223 | issueToken(txn.client, txn.user, txn.res, txn.req, issued); 224 | } else if (arity == 4) { 225 | issueToken(txn.client, txn.user, txn.res, issued); 226 | } else { // arity == 3 227 | issueToken(txn.client, txn.user, issued); 228 | } 229 | } catch (ex) { 230 | return next(ex); 231 | } 232 | } 233 | 234 | doIssueToken(); 235 | } 236 | 237 | function errorHandler(err, txn, res, next) { 238 | var mode = 'fragment' 239 | , params = {} 240 | , respond; 241 | if (txn.req && txn.req.responseMode) { 242 | mode = txn.req.responseMode; 243 | } 244 | respond = modes[mode]; 245 | 246 | if (!respond) { 247 | return next(err); 248 | } 249 | if (respond && respond.validate) { 250 | try { 251 | respond.validate(txn); 252 | } catch(ex) { 253 | return next(err); 254 | } 255 | } 256 | 257 | params.error = err.code || 'server_error'; 258 | if (err.message) { params.error_description = err.message; } 259 | if (err.uri) { params.error_uri = err.uri; } 260 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 261 | return respond(txn, res, params); 262 | } 263 | 264 | /** 265 | * Return `code id_token token` grant module. 266 | */ 267 | var mod = {}; 268 | mod.name = 'code id_token token'; 269 | mod.request = request; 270 | mod.response = response; 271 | mod.error = errorHandler; 272 | return mod; 273 | } 274 | -------------------------------------------------------------------------------- /lib/grant/codeToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var merge = require('utils-merge') 5 | , AuthorizationError = require('../errors/authorizationerror'); 6 | 7 | 8 | /** 9 | * Handles requests to obtain a response with an access token and authorization 10 | * code. 11 | * 12 | * References: 13 | * - [OpenID Connect Standard 1.0 - draft 21](http://openid.net/specs/openid-connect-standard-1_0.html) 14 | * - [OpenID Connect Messages 1.0 - draft 20](http://openid.net/specs/openid-connect-messages-1_0.html) 15 | * - [OAuth 2.0 Multiple Response Type Encoding Practices - draft 08](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) 16 | * 17 | * @param {Object} options 18 | * @param {Function} issue 19 | * @return {Object} module 20 | * @api public 21 | */ 22 | module.exports = function(options, issueToken, issueCode) { 23 | if (typeof options == 'function') { 24 | issueCode = issueToken; 25 | issueToken = options; 26 | options = undefined; 27 | } 28 | options = options || {}; 29 | 30 | if (!issueToken) throw new TypeError('oauth2orize-openid.codeToken grant requires an issueToken callback'); 31 | if (!issueCode) throw new TypeError('oauth2orize-openid.codeToken grant requires an issueCode callback'); 32 | 33 | var modes = options.modes || {}; 34 | if (!modes.fragment) { 35 | modes.fragment = require('../response/fragment'); 36 | } 37 | 38 | // For maximum flexibility, multiple scope spearators can optionally be 39 | // allowed. This allows the server to accept clients that separate scope 40 | // with either space or comma (' ', ','). This violates the specification, 41 | // but achieves compatibility with existing client libraries that are already 42 | // deployed. 43 | var separators = options.scopeSeparator || ' '; 44 | if (!Array.isArray(separators)) { 45 | separators = [ separators ]; 46 | } 47 | 48 | 49 | /* Parse requests that request `code token` as `response_type`. 50 | * 51 | * @param {http.ServerRequest} req 52 | * @api public 53 | */ 54 | function request(req) { 55 | var clientID = req.query['client_id'] 56 | , redirectURI = req.query['redirect_uri'] 57 | , scope = req.query['scope'] 58 | , state = req.query['state']; 59 | 60 | if (!clientID) { throw new AuthorizationError('Missing required parameter: client_id', 'invalid_request'); } 61 | if (typeof clientID !== 'string') { throw new AuthorizationError('Invalid parameter: client_id must be a string', 'invalid_request'); } 62 | 63 | if (scope) { 64 | if (typeof scope !== 'string') { 65 | throw new AuthorizationError('Invalid parameter: scope must be a string', 'invalid_request'); 66 | } 67 | 68 | for (var i = 0, len = separators.length; i < len; i++) { 69 | var separated = scope.split(separators[i]); 70 | // only separate on the first matching separator. this allows for a sort 71 | // of separator "priority" (ie, favor spaces then fallback to commas) 72 | if (separated.length > 1) { 73 | scope = separated; 74 | break; 75 | } 76 | } 77 | 78 | if (!Array.isArray(scope)) { scope = [ scope ]; } 79 | } 80 | 81 | return { 82 | clientID: clientID, 83 | redirectURI: redirectURI, 84 | scope: scope, 85 | state: state 86 | } 87 | } 88 | 89 | /* Sends responses to transactions that request `code token` as `response_type`. 90 | * 91 | * @param {Object} txn 92 | * @param {http.ServerResponse} res 93 | * @param {Function} complete 94 | * @param {Function} next 95 | * @api public 96 | */ 97 | function response(txn, res, complete, next) { 98 | if (next === undefined) { 99 | next = complete; 100 | complete = function(cb) { return cb(); }; 101 | } 102 | 103 | var mode = 'fragment' 104 | , respond; 105 | if (txn.req && txn.req.responseMode) { 106 | mode = txn.req.responseMode; 107 | } 108 | respond = modes[mode]; 109 | 110 | if (!respond) { 111 | // http://lists.openid.net/pipermail/openid-specs-ab/Week-of-Mon-20140317/004680.html 112 | return next(new AuthorizationError('Unsupported response mode: ' + mode, 'unsupported_response_mode', null, 501)); 113 | } 114 | if (respond && respond.validate) { 115 | try { 116 | respond.validate(txn); 117 | } catch(ex) { 118 | return next(ex); 119 | } 120 | } 121 | 122 | if (!txn.res.allow) { 123 | var params = { error: 'access_denied' }; 124 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 125 | return respond(txn, res, params); 126 | } 127 | 128 | function doIssueCode(tok) { 129 | function issued(err, code) { 130 | if (err) { return next(err); } 131 | if (!code) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 132 | 133 | tok['code'] = code; 134 | 135 | complete(function(err) { 136 | if (err) { return next(err); } 137 | return respond(txn, res, tok); 138 | }); 139 | } 140 | 141 | try { 142 | // NOTE: To facilitate code reuse, the `issueCode` callback should 143 | // interoperate with the `issue` callback implemented by 144 | // `oauth2orize.grant.code`. 145 | 146 | var arity = issueCode.length; 147 | if (arity == 7) { 148 | var locals = txn.locals || {}; 149 | locals.accessToken = tok.access_token; 150 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, locals, issued); 151 | } else if (arity == 6) { 152 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, issued); 153 | } else if (arity == 5) { 154 | issueCode(txn.client, txn.req.redirectURI, txn.user, txn.res, issued); 155 | } else { // arity == 4 156 | issueCode(txn.client, txn.req.redirectURI, txn.user, issued); 157 | } 158 | } catch (ex) { 159 | return next(ex); 160 | } 161 | } 162 | 163 | function doIssueToken() { 164 | function issued(err, accessToken, params) { 165 | if (err) { return next(err); } 166 | if (!accessToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 167 | 168 | var tok = {}; 169 | tok['access_token'] = accessToken; 170 | if (params) { merge(tok, params); } 171 | tok['token_type'] = tok['token_type'] || 'Bearer'; 172 | if (txn.req && txn.req.state) { tok['state'] = txn.req.state; } 173 | 174 | doIssueCode(tok); 175 | } 176 | 177 | try { 178 | // NOTE: To facilitate code reuse, the `issueToken` callback should 179 | // interoperate with the `issue` callback implemented by 180 | // `oauth2orize.grant.token`. 181 | 182 | var arity = issueToken.length; 183 | if (arity == 6) { 184 | issueToken(txn.client, txn.user, txn.res, txn.req, txn.locals, issued); 185 | } else if (arity == 5) { 186 | issueToken(txn.client, txn.user, txn.res, txn.req, issued); 187 | } else if (arity == 4) { 188 | issueToken(txn.client, txn.user, txn.res, issued); 189 | } else { // arity == 3 190 | issueToken(txn.client, txn.user, issued); 191 | } 192 | } catch (ex) { 193 | return next(ex); 194 | } 195 | } 196 | 197 | doIssueToken(); 198 | } 199 | 200 | function errorHandler(err, txn, res, next) { 201 | var mode = 'fragment' 202 | , params = {} 203 | , respond; 204 | if (txn.req && txn.req.responseMode) { 205 | mode = txn.req.responseMode; 206 | } 207 | respond = modes[mode]; 208 | 209 | if (!respond) { 210 | return next(err); 211 | } 212 | if (respond && respond.validate) { 213 | try { 214 | respond.validate(txn); 215 | } catch(ex) { 216 | return next(err); 217 | } 218 | } 219 | 220 | params.error = err.code || 'server_error'; 221 | if (err.message) { params.error_description = err.message; } 222 | if (err.uri) { params.error_uri = err.uri; } 223 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 224 | return respond(txn, res, params); 225 | } 226 | 227 | /** 228 | * Return `code token` grant module. 229 | */ 230 | var mod = {}; 231 | mod.name = 'code token'; 232 | mod.request = request; 233 | mod.response = response; 234 | mod.error = errorHandler; 235 | return mod; 236 | } 237 | -------------------------------------------------------------------------------- /lib/grant/idToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var AuthorizationError = require('../errors/authorizationerror'); 5 | 6 | 7 | /** 8 | * Handles requests to obtain a response with an ID token. 9 | * 10 | * References: 11 | * - [OpenID Connect Standard 1.0 - draft 21](http://openid.net/specs/openid-connect-standard-1_0.html) 12 | * - [OpenID Connect Messages 1.0 - draft 20](http://openid.net/specs/openid-connect-messages-1_0.html) 13 | * - [OAuth 2.0 Multiple Response Type Encoding Practices - draft 08](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) 14 | * 15 | * @param {Object} options 16 | * @param {Function} issue 17 | * @return {Object} module 18 | * @api public 19 | */ 20 | module.exports = function(options, issue) { 21 | if (typeof options == 'function') { 22 | issue = options; 23 | options = undefined; 24 | } 25 | options = options || {}; 26 | 27 | if (!issue) throw new TypeError('oauth2orize-openid.idToken grant requires an issue callback'); 28 | 29 | var modes = options.modes || {}; 30 | if (!modes.fragment) { 31 | modes.fragment = require('../response/fragment'); 32 | } 33 | 34 | // For maximum flexibility, multiple scope spearators can optionally be 35 | // allowed. This allows the server to accept clients that separate scope 36 | // with either space or comma (' ', ','). This violates the specification, 37 | // but achieves compatibility with existing client libraries that are already 38 | // deployed. 39 | var separators = options.scopeSeparator || ' '; 40 | if (!Array.isArray(separators)) { 41 | separators = [ separators ]; 42 | } 43 | 44 | 45 | /* Parse requests that request `id_token` as `response_type`. 46 | * 47 | * @param {http.ServerRequest} req 48 | * @api public 49 | */ 50 | function request(req) { 51 | var clientID = req.query['client_id'] 52 | , redirectURI = req.query['redirect_uri'] 53 | , scope = req.query['scope'] 54 | , state = req.query['state'] 55 | , nonce = req.query['nonce']; 56 | 57 | if (!clientID) { throw new AuthorizationError('Missing required parameter: client_id', 'invalid_request'); } 58 | if (typeof clientID !== 'string') { throw new AuthorizationError('Invalid parameter: client_id must be a string', 'invalid_request'); } 59 | 60 | if (!nonce) { throw new AuthorizationError('Missing required parameter: nonce', 'invalid_request'); } 61 | if (typeof nonce !== 'string') { throw new AuthorizationError('Invalid parameter: nonce must be a string', 'invalid_request'); } 62 | 63 | 64 | if (scope) { 65 | if (typeof scope !== 'string') { 66 | throw new AuthorizationError('Invalid parameter: scope must be a string', 'invalid_request'); 67 | } 68 | 69 | for (var i = 0, len = separators.length; i < len; i++) { 70 | var separated = scope.split(separators[i]); 71 | // only separate on the first matching separator. this allows for a sort 72 | // of separator "priority" (ie, favor spaces then fallback to commas) 73 | if (separated.length > 1) { 74 | scope = separated; 75 | break; 76 | } 77 | } 78 | 79 | if (!Array.isArray(scope)) { scope = [ scope ]; } 80 | } 81 | 82 | return { 83 | clientID: clientID, 84 | redirectURI: redirectURI, 85 | scope: scope, 86 | state: state, 87 | nonce: nonce 88 | } 89 | } 90 | 91 | /* Sends responses to transactions that request `id_token` as `response_type`. 92 | * 93 | * @param {Object} txn 94 | * @param {http.ServerResponse} res 95 | * @param {Function} complete 96 | * @param {Function} next 97 | * @api public 98 | */ 99 | function response(txn, res, complete, next) { 100 | if (next === undefined) { 101 | next = complete; 102 | complete = function(cb) { return cb(); }; 103 | } 104 | 105 | var mode = 'fragment' 106 | , respond; 107 | if (txn.req && txn.req.responseMode) { 108 | mode = txn.req.responseMode; 109 | } 110 | respond = modes[mode]; 111 | 112 | if (!respond) { 113 | // http://lists.openid.net/pipermail/openid-specs-ab/Week-of-Mon-20140317/004680.html 114 | return next(new AuthorizationError('Unsupported response mode: ' + mode, 'unsupported_response_mode', null, 501)); 115 | } 116 | if (respond && respond.validate) { 117 | try { 118 | respond.validate(txn); 119 | } catch(ex) { 120 | return next(ex); 121 | } 122 | } 123 | 124 | if (!txn.res.allow) { 125 | var params = { error: 'access_denied' }; 126 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 127 | return respond(txn, res, params); 128 | } 129 | 130 | function issued(err, idToken) { 131 | if (err) { return next(err); } 132 | if (!idToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 133 | 134 | var tok = {}; 135 | tok['id_token'] = idToken; 136 | if (txn.req && txn.req.state) { tok['state'] = txn.req.state; } 137 | 138 | complete(function(err) { 139 | if (err) { return next(err); } 140 | return respond(txn, res, tok); 141 | }); 142 | } 143 | 144 | try { 145 | var arity = issue.length; 146 | if (arity == 7) { 147 | issue(txn.client, txn.user, txn.res, txn.req, undefined, txn.locals, issued); 148 | } else if (arity == 6) { 149 | issue(txn.client, txn.user, txn.res, txn.req, undefined, issued); 150 | } else if (arity == 5) { 151 | issue(txn.client, txn.user, txn.res, txn.req, issued); 152 | } else { // arity == 4 153 | issue(txn.client, txn.user, txn.req, issued); 154 | } 155 | } catch (ex) { 156 | return next(ex); 157 | } 158 | } 159 | 160 | function errorHandler(err, txn, res, next) { 161 | var mode = 'fragment' 162 | , params = {} 163 | , respond; 164 | if (txn.req && txn.req.responseMode) { 165 | mode = txn.req.responseMode; 166 | } 167 | respond = modes[mode]; 168 | 169 | if (!respond) { 170 | return next(err); 171 | } 172 | if (respond && respond.validate) { 173 | try { 174 | respond.validate(txn); 175 | } catch(ex) { 176 | return next(err); 177 | } 178 | } 179 | 180 | params.error = err.code || 'server_error'; 181 | if (err.message) { params.error_description = err.message; } 182 | if (err.uri) { params.error_uri = err.uri; } 183 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 184 | return respond(txn, res, params); 185 | } 186 | 187 | /** 188 | * Return `id_token` grant module. 189 | */ 190 | var mod = {}; 191 | mod.name = 'id_token'; 192 | mod.request = request; 193 | mod.response = response; 194 | mod.error = errorHandler; 195 | return mod; 196 | } 197 | -------------------------------------------------------------------------------- /lib/grant/idTokenToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var merge = require('utils-merge') 5 | , AuthorizationError = require('../errors/authorizationerror'); 6 | 7 | 8 | /** 9 | * Handles requests to obtain a response with an access token and ID token. 10 | * 11 | * References: 12 | * - [OpenID Connect Standard 1.0 - draft 21](http://openid.net/specs/openid-connect-standard-1_0.html) 13 | * - [OpenID Connect Messages 1.0 - draft 20](http://openid.net/specs/openid-connect-messages-1_0.html) 14 | * - [OAuth 2.0 Multiple Response Type Encoding Practices - draft 08](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) 15 | * 16 | * @param {Object} options 17 | * @param {Function} issue 18 | * @return {Object} module 19 | * @api public 20 | */ 21 | module.exports = function(options, issueToken, issueIDToken) { 22 | if (typeof options == 'function') { 23 | issueIDToken = issueToken; 24 | issueToken = options; 25 | options = undefined; 26 | } 27 | options = options || {}; 28 | 29 | if (!issueToken) throw new TypeError('oauth2orize-openid.idTokenToken grant requires an issueToken callback'); 30 | if (!issueIDToken) throw new TypeError('oauth2orize-openid.idTokenToken grant requires an issueIDToken callback'); 31 | 32 | var modes = options.modes || {}; 33 | if (!modes.fragment) { 34 | modes.fragment = require('../response/fragment'); 35 | } 36 | 37 | // For maximum flexibility, multiple scope spearators can optionally be 38 | // allowed. This allows the server to accept clients that separate scope 39 | // with either space or comma (' ', ','). This violates the specification, 40 | // but achieves compatibility with existing client libraries that are already 41 | // deployed. 42 | var separators = options.scopeSeparator || ' '; 43 | if (!Array.isArray(separators)) { 44 | separators = [ separators ]; 45 | } 46 | 47 | 48 | /* Parse requests that request `id_token token` as `response_type`. 49 | * 50 | * @param {http.ServerRequest} req 51 | * @api public 52 | */ 53 | function request(req) { 54 | var clientID = req.query['client_id'] 55 | , redirectURI = req.query['redirect_uri'] 56 | , scope = req.query['scope'] 57 | , state = req.query['state'] 58 | , nonce = req.query['nonce']; 59 | 60 | if (!clientID) { throw new AuthorizationError('Missing required parameter: client_id', 'invalid_request'); } 61 | if (typeof clientID !== 'string') { throw new AuthorizationError('Invalid parameter: client_id must be a string', 'invalid_request'); } 62 | 63 | if (!nonce) { throw new AuthorizationError('Missing required parameter: nonce', 'invalid_request'); } 64 | if (typeof nonce !== 'string') { throw new AuthorizationError('Invalid parameter: nonce must be a string', 'invalid_request'); } 65 | 66 | if (scope) { 67 | if (typeof scope !== 'string') { 68 | throw new AuthorizationError('Invalid parameter: scope must be a string', 'invalid_request'); 69 | } 70 | 71 | for (var i = 0, len = separators.length; i < len; i++) { 72 | var separated = scope.split(separators[i]); 73 | // only separate on the first matching separator. this allows for a sort 74 | // of separator "priority" (ie, favor spaces then fallback to commas) 75 | if (separated.length > 1) { 76 | scope = separated; 77 | break; 78 | } 79 | } 80 | 81 | if (!Array.isArray(scope)) { scope = [ scope ]; } 82 | } 83 | 84 | return { 85 | clientID: clientID, 86 | redirectURI: redirectURI, 87 | scope: scope, 88 | state: state, 89 | nonce: nonce 90 | } 91 | } 92 | 93 | /* Sends responses to transactions that request `id_token token` as `response_type`. 94 | * 95 | * @param {Object} txn 96 | * @param {http.ServerResponse} res 97 | * @param {Function} complete 98 | * @param {Function} next 99 | * @api public 100 | */ 101 | function response(txn, res, complete, next) { 102 | if (next === undefined) { 103 | next = complete; 104 | complete = function(cb) { return cb(); }; 105 | } 106 | 107 | var mode = 'fragment' 108 | , respond; 109 | 110 | if (txn.req && txn.req.responseMode) { 111 | mode = txn.req.responseMode; 112 | } 113 | respond = modes[mode]; 114 | 115 | if (!respond) { 116 | // http://lists.openid.net/pipermail/openid-specs-ab/Week-of-Mon-20140317/004680.html 117 | return next(new AuthorizationError('Unsupported response mode: ' + mode, 'unsupported_response_mode', null, 501)); 118 | } 119 | if (respond && respond.validate) { 120 | try { 121 | respond.validate(txn); 122 | } catch(ex) { 123 | return next(ex); 124 | } 125 | } 126 | 127 | if (!txn.res.allow) { 128 | var params = { error: 'access_denied' }; 129 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 130 | return respond(txn, res, params); 131 | } 132 | 133 | function doIssueIDToken(tok) { 134 | function issued(err, idToken) { 135 | if (err) { return next(err); } 136 | if (!idToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 137 | 138 | tok['id_token'] = idToken; 139 | if (txn.req && txn.req.state) { tok['state'] = txn.req.state; } 140 | 141 | complete(function(err) { 142 | if (err) { return next(err); } 143 | return respond(txn, res, tok); 144 | }); 145 | } 146 | 147 | try { 148 | // NOTE: To facilitate code reuse, the `issueIDToken` callback should 149 | // interoperate with the `issue` callback implemented by 150 | // `oauth2orize-openid.grant.idToken`. 151 | 152 | var arity = issueIDToken.length; 153 | if (arity == 7) { 154 | issueIDToken(txn.client, txn.user, txn.res, txn.req, { accessToken: tok.access_token }, txn.locals, issued); 155 | } else if (arity == 6) { 156 | issueIDToken(txn.client, txn.user, txn.res, txn.req, { accessToken: tok.access_token }, issued); 157 | } else if (arity == 5) { 158 | issueIDToken(txn.client, txn.user, txn.res, txn.req, issued); 159 | } else { // arity == 4 160 | issueIDToken(txn.client, txn.user, txn.req, issued); 161 | } 162 | } catch (ex) { 163 | return next(ex); 164 | } 165 | } 166 | 167 | function doIssueToken() { 168 | function issued(err, accessToken, params) { 169 | if (err) { return next(err); } 170 | if (!accessToken) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); } 171 | 172 | var tok = {}; 173 | tok['access_token'] = accessToken; 174 | if (params) { merge(tok, params); } 175 | tok['token_type'] = tok['token_type'] || 'Bearer'; 176 | if (txn.req && txn.req.state) { tok['state'] = txn.req.state; } 177 | 178 | doIssueIDToken(tok); 179 | } 180 | 181 | try { 182 | // NOTE: To facilitate code reuse, the `issueToken` callback should 183 | // interoperate with the `issue` callback implemented by 184 | // `oauth2orize.grant.token`. 185 | 186 | var arity = issueToken.length; 187 | if (arity == 6) { 188 | issueToken(txn.client, txn.user, txn.res, txn.req, txn.locals, issued); 189 | } else if (arity == 5) { 190 | issueToken(txn.client, txn.user, txn.res, txn.req, issued); 191 | } else if (arity == 4) { 192 | issueToken(txn.client, txn.user, txn.res, issued); 193 | } else { // arity == 3 194 | issueToken(txn.client, txn.user, issued); 195 | } 196 | } catch (ex) { 197 | return next(ex); 198 | } 199 | } 200 | 201 | doIssueToken(); 202 | } 203 | 204 | function errorHandler(err, txn, res, next) { 205 | var mode = 'fragment' 206 | , params = {} 207 | , respond; 208 | if (txn.req && txn.req.responseMode) { 209 | mode = txn.req.responseMode; 210 | } 211 | respond = modes[mode]; 212 | 213 | if (!respond) { 214 | return next(err); 215 | } 216 | if (respond && respond.validate) { 217 | try { 218 | respond.validate(txn); 219 | } catch(ex) { 220 | return next(err); 221 | } 222 | } 223 | 224 | params.error = err.code || 'server_error'; 225 | if (err.message) { params.error_description = err.message; } 226 | if (err.uri) { params.error_uri = err.uri; } 227 | if (txn.req && txn.req.state) { params.state = txn.req.state; } 228 | return respond(txn, res, params); 229 | } 230 | 231 | /** 232 | * Return `id_token token` grant module. 233 | */ 234 | var mod = {}; 235 | mod.name = 'id_token token'; 236 | mod.request = request; 237 | mod.response = response; 238 | mod.error = errorHandler; 239 | return mod; 240 | } 241 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.extensions = require('./request/extensions'); 2 | 3 | exports.grant = {}; 4 | exports.grant.idToken = require('./grant/idToken'); 5 | exports.grant.idTokenToken = require('./grant/idTokenToken'); 6 | exports.grant.codeToken = require('./grant/codeToken'); 7 | exports.grant.codeIdToken = 8 | exports.grant.codeIDToken = require('./grant/codeIdToken'); 9 | exports.grant.codeIdTokenToken = 10 | exports.grant.codeIDTokenToken = require('./grant/codeIdTokenToken'); 11 | -------------------------------------------------------------------------------- /lib/request/extensions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var AuthorizationError = require('../errors/authorizationerror'); 5 | 6 | /** 7 | * Parse request parameters defined by OpenID Connect. 8 | * 9 | * This module is a wildcard parser that parses authorization requests for 10 | * extensions parameters defined by OpenID Connect. 11 | * 12 | * Examples: 13 | * 14 | * server.grant(openid.extensions()); 15 | * 16 | * References: 17 | * - [OpenID Connect Basic Client Profile 1.0 - draft 28](http://openid.net/specs/openid-connect-basic-1_0.html) 18 | * - [OpenID Connect Implicit Client Profile 1.0 - draft 11](http://openid.net/specs/openid-connect-implicit-1_0.html) 19 | * - [OpenID Connect Messages 1.0 - draft 20](http://openid.net/specs/openid-connect-messages-1_0.html) 20 | * 21 | * @return {Object} module 22 | * @api public 23 | */ 24 | module.exports = function() { 25 | 26 | function request(req) { 27 | var q = req.query 28 | , ext = {}; 29 | 30 | // TODO: Only parse these if scope includes `openid` 31 | 32 | function parse(param, split) { 33 | if (!q[param]) { return; } 34 | 35 | if (typeof q[param] !== 'string') { 36 | throw new AuthorizationError('Failed to parse ' + param + ' as string', 'invalid_request'); 37 | } 38 | 39 | return (split) ? q[param].split(' ') : q[param]; 40 | } 41 | 42 | ext.nonce = parse('nonce'); 43 | ext.display = parse('display') || 'page'; 44 | if (q.prompt) { ext.prompt = parse('prompt', true); } 45 | if (q.max_age) { ext.maxAge = parseInt(q.max_age); } 46 | if (q.ui_locales) { ext.uiLocales = parse('ui_locales', true); } 47 | if (q.claims_locales) { ext.claimsLocales = parse('claims_locales', true); } 48 | ext.idTokenHint = q.id_token_hint; 49 | ext.loginHint = q.login_hint; 50 | if (q.acr_values) { ext.acrValues = parse('acr_values', true); } 51 | 52 | if (q.claims) { 53 | try { 54 | ext.claims = JSON.parse(q.claims); 55 | } catch (_) { 56 | throw new AuthorizationError('Failed to parse claims as JSON', 'invalid_request'); 57 | } 58 | } 59 | 60 | if (q.registration) { 61 | try { 62 | ext.registration = JSON.parse(q.registration); 63 | } catch (_) { 64 | throw new AuthorizationError('Failed to parse registration as JSON', 'invalid_request'); 65 | } 66 | } 67 | 68 | // NOTE: The below parameters should be implemented in a separate extension, tracking 69 | // the following IETF draft: 70 | // https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-06 71 | // http://openid.net/specs/openid-connect-core-1_0.html#JWTRequests 72 | // TODO: Add support for "request" parameter 73 | // TODO: Add support for "request_uri" parameter 74 | 75 | if (ext.prompt && ext.prompt.length > 1) { 76 | if (ext.prompt.indexOf('none') != -1) { throw new AuthorizationError('Prompt includes none with other values', 'invalid_request'); } 77 | } 78 | 79 | return ext; 80 | } 81 | 82 | var mod = {}; 83 | mod.name = '*'; 84 | mod.request = request; 85 | return mod; 86 | } 87 | -------------------------------------------------------------------------------- /lib/response/fragment.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | , qs = require('querystring') 3 | , AuthorizationError = require('../errors/authorizationerror'); 4 | 5 | /** 6 | * Authorization Response parameters are encoded in the fragment added to the redirect_uri when 7 | * redirecting back to the Client. 8 | **/ 9 | exports = module.exports = function (txn, res, params) { 10 | var parsed = url.parse(txn.redirectURI); 11 | parsed.hash = qs.stringify(params); 12 | 13 | var location = url.format(parsed); 14 | return res.redirect(location); 15 | }; 16 | 17 | 18 | exports.validate = function(txn) { 19 | if (!txn.redirectURI) { throw new AuthorizationError('Unable to issue redirect for OAuth 2.0 transaction', 'server_error'); } 20 | }; 21 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2orize-openid", 3 | "version": "0.4.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "assertion-error": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", 10 | "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", 11 | "dev": true 12 | }, 13 | "chai": { 14 | "version": "2.3.0", 15 | "resolved": "https://registry.npmjs.org/chai/-/chai-2.3.0.tgz", 16 | "integrity": "sha1-ii9qNHSNqAEJD9cyh7Kqc5pOkJo=", 17 | "dev": true, 18 | "requires": { 19 | "assertion-error": "1.0.0", 20 | "deep-eql": "0.1.3" 21 | } 22 | }, 23 | "chai-connect-middleware": { 24 | "version": "0.3.1", 25 | "resolved": "https://registry.npmjs.org/chai-connect-middleware/-/chai-connect-middleware-0.3.1.tgz", 26 | "integrity": "sha1-6qGF6ZKhAtAyvW4ngAEqjmQ1hqg=", 27 | "dev": true 28 | }, 29 | "chai-oauth2orize-grant": { 30 | "version": "0.2.1", 31 | "resolved": "https://registry.npmjs.org/chai-oauth2orize-grant/-/chai-oauth2orize-grant-0.2.1.tgz", 32 | "integrity": "sha1-bVRhUZMCbsZWlO/S8vxcXKa6TMI=", 33 | "dev": true 34 | }, 35 | "commander": { 36 | "version": "2.3.0", 37 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", 38 | "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", 39 | "dev": true 40 | }, 41 | "debug": { 42 | "version": "2.2.0", 43 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 44 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 45 | "dev": true, 46 | "requires": { 47 | "ms": "0.7.1" 48 | } 49 | }, 50 | "deep-eql": { 51 | "version": "0.1.3", 52 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", 53 | "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", 54 | "dev": true, 55 | "requires": { 56 | "type-detect": "0.1.1" 57 | } 58 | }, 59 | "diff": { 60 | "version": "1.4.0", 61 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", 62 | "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", 63 | "dev": true 64 | }, 65 | "escape-string-regexp": { 66 | "version": "1.0.2", 67 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", 68 | "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", 69 | "dev": true 70 | }, 71 | "glob": { 72 | "version": "3.2.11", 73 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 74 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 75 | "dev": true, 76 | "requires": { 77 | "inherits": "2", 78 | "minimatch": "0.3" 79 | } 80 | }, 81 | "growl": { 82 | "version": "1.9.2", 83 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 84 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 85 | "dev": true 86 | }, 87 | "inherits": { 88 | "version": "2.0.4", 89 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 90 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 91 | "dev": true 92 | }, 93 | "jade": { 94 | "version": "0.26.3", 95 | "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", 96 | "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", 97 | "dev": true, 98 | "requires": { 99 | "commander": "0.6.1", 100 | "mkdirp": "0.3.0" 101 | }, 102 | "dependencies": { 103 | "commander": { 104 | "version": "0.6.1", 105 | "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", 106 | "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", 107 | "dev": true 108 | }, 109 | "mkdirp": { 110 | "version": "0.3.0", 111 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", 112 | "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", 113 | "dev": true 114 | } 115 | } 116 | }, 117 | "lru-cache": { 118 | "version": "2.7.3", 119 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 120 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 121 | "dev": true 122 | }, 123 | "make-node": { 124 | "version": "0.3.5", 125 | "resolved": "https://registry.npmjs.org/make-node/-/make-node-0.3.5.tgz", 126 | "integrity": "sha1-LTVN240+zfWg1btMrbuqRGHK3jo=", 127 | "dev": true 128 | }, 129 | "minimatch": { 130 | "version": "0.3.0", 131 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 132 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 133 | "dev": true, 134 | "requires": { 135 | "lru-cache": "2", 136 | "sigmund": "~1.0.0" 137 | } 138 | }, 139 | "minimist": { 140 | "version": "0.0.8", 141 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 142 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 143 | "dev": true 144 | }, 145 | "mkdirp": { 146 | "version": "0.5.1", 147 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 148 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 149 | "dev": true, 150 | "requires": { 151 | "minimist": "0.0.8" 152 | } 153 | }, 154 | "mocha": { 155 | "version": "2.5.3", 156 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", 157 | "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", 158 | "dev": true, 159 | "requires": { 160 | "commander": "2.3.0", 161 | "debug": "2.2.0", 162 | "diff": "1.4.0", 163 | "escape-string-regexp": "1.0.2", 164 | "glob": "3.2.11", 165 | "growl": "1.9.2", 166 | "jade": "0.26.3", 167 | "mkdirp": "0.5.1", 168 | "supports-color": "1.2.0", 169 | "to-iso-string": "0.0.2" 170 | } 171 | }, 172 | "ms": { 173 | "version": "0.7.1", 174 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 175 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", 176 | "dev": true 177 | }, 178 | "sigmund": { 179 | "version": "1.0.1", 180 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 181 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 182 | "dev": true 183 | }, 184 | "supports-color": { 185 | "version": "1.2.0", 186 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", 187 | "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", 188 | "dev": true 189 | }, 190 | "to-iso-string": { 191 | "version": "0.0.2", 192 | "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", 193 | "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", 194 | "dev": true 195 | }, 196 | "type-detect": { 197 | "version": "0.1.1", 198 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", 199 | "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", 200 | "dev": true 201 | }, 202 | "utils-merge": { 203 | "version": "1.0.1", 204 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 205 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2orize-openid", 3 | "version": "0.4.1", 4 | "description": "Extensions to support OpenID Connect with OAuth2orize.", 5 | "keywords": [ 6 | "openid", 7 | "openidconnect", 8 | "oidc", 9 | "oauth", 10 | "oauth2", 11 | "auth", 12 | "authn", 13 | "authentication", 14 | "authz", 15 | "authorization", 16 | "connect", 17 | "express", 18 | "passport", 19 | "middleware" 20 | ], 21 | "author": { 22 | "name": "Jared Hanson", 23 | "email": "jaredhanson@gmail.com", 24 | "url": "https://www.jaredhanson.me/" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/jaredhanson/oauth2orize-openid.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/jaredhanson/oauth2orize-openid/issues" 32 | }, 33 | "funding": { 34 | "type": "github", 35 | "url": "https://github.com/sponsors/jaredhanson" 36 | }, 37 | "license": "MIT", 38 | "licenses": [ 39 | { 40 | "type": "MIT", 41 | "url": "https://opensource.org/licenses/MIT" 42 | } 43 | ], 44 | "main": "./lib", 45 | "dependencies": { 46 | "utils-merge": "1.x.x" 47 | }, 48 | "devDependencies": { 49 | "make-node": "0.3.x", 50 | "mocha": "2.x.x", 51 | "chai": "2.x.x", 52 | "chai-connect-middleware": "0.3.x", 53 | "chai-oauth2orize-grant": "0.2.x" 54 | }, 55 | "engines": { 56 | "node": ">= 0.4.0" 57 | }, 58 | "scripts": { 59 | "test": "node_modules/.bin/mocha --reporter spec --require test/bootstrap/node test/*.test.js test/**/*.test.js" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/bootstrap/node.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | 3 | chai.use(require('chai-connect-middleware')); 4 | chai.use(require('chai-oauth2orize-grant')); 5 | 6 | global.expect = chai.expect; -------------------------------------------------------------------------------- /test/grant/codeIdToken.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , codeIdToken = require('../../lib/grant/codeIdToken'); 3 | 4 | 5 | describe('grant.codeIdToken', function() { 6 | 7 | describe('module', function() { 8 | var mod = codeIdToken(function(){}, function(){}); 9 | 10 | it('should be named code id_token', function() { 11 | expect(mod.name).to.equal('code id_token'); 12 | }); 13 | 14 | it('should expose request and response functions', function() { 15 | expect(mod.request).to.be.a('function'); 16 | expect(mod.response).to.be.a('function'); 17 | }); 18 | }); 19 | 20 | it('should throw if constructed without a issueCode callback', function() { 21 | expect(function() { 22 | codeIdToken(); 23 | }).to.throw(TypeError, 'oauth2orize-openid.codeIDToken grant requires an issueCode callback'); 24 | }); 25 | 26 | it('should throw if constructed without a issueIDToken callback', function() { 27 | expect(function() { 28 | codeIdToken(function(){}); 29 | }).to.throw(TypeError, 'oauth2orize-openid.codeIDToken grant requires an issueIDToken callback'); 30 | }); 31 | 32 | describe('request parsing', function() { 33 | function issueCode(){} 34 | function issueIDToken(){} 35 | 36 | describe('request', function() { 37 | var err, out; 38 | 39 | before(function(done) { 40 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 41 | .req(function(req) { 42 | req.query = {}; 43 | req.query.client_id = 'c123'; 44 | req.query.redirect_uri = 'http://example.com/auth/callback'; 45 | req.query.state = 'f1o1o1'; 46 | req.query.nonce = 'n123'; 47 | }) 48 | .parse(function(e, o) { 49 | err = e; 50 | out = o; 51 | done(); 52 | }) 53 | .authorize(); 54 | }); 55 | 56 | it('should not error', function() { 57 | expect(err).to.be.null; 58 | }); 59 | 60 | it('should parse request', function() { 61 | expect(out.clientID).to.equal('c123'); 62 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 63 | expect(out.scope).to.be.undefined; 64 | expect(out.state).to.equal('f1o1o1'); 65 | expect(out.nonce).to.equal('n123'); 66 | }); 67 | }); 68 | 69 | describe('request with scope', function() { 70 | var err, out; 71 | 72 | before(function(done) { 73 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 74 | .req(function(req) { 75 | req.query = {}; 76 | req.query.client_id = 'c123'; 77 | req.query.redirect_uri = 'http://example.com/auth/callback'; 78 | req.query.scope = 'read'; 79 | req.query.state = 'f1o1o1'; 80 | req.query.nonce = 'n123'; 81 | }) 82 | .parse(function(e, o) { 83 | err = e; 84 | out = o; 85 | done(); 86 | }) 87 | .authorize(); 88 | }); 89 | 90 | it('should not error', function() { 91 | expect(err).to.be.null; 92 | }); 93 | 94 | it('should parse request', function() { 95 | expect(out.clientID).to.equal('c123'); 96 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 97 | expect(out.scope).to.be.an('array'); 98 | expect(out.scope).to.have.length(1); 99 | expect(out.scope[0]).to.equal('read'); 100 | expect(out.state).to.equal('f1o1o1'); 101 | expect(out.nonce).to.equal('n123'); 102 | }); 103 | }); 104 | 105 | describe('request with list of scopes', function() { 106 | var err, out; 107 | 108 | before(function(done) { 109 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 110 | .req(function(req) { 111 | req.query = {}; 112 | req.query.client_id = 'c123'; 113 | req.query.redirect_uri = 'http://example.com/auth/callback'; 114 | req.query.scope = 'read write'; 115 | req.query.state = 'f1o1o1'; 116 | req.query.nonce = 'n123'; 117 | }) 118 | .parse(function(e, o) { 119 | err = e; 120 | out = o; 121 | done(); 122 | }) 123 | .authorize(); 124 | }); 125 | 126 | it('should not error', function() { 127 | expect(err).to.be.null; 128 | }); 129 | 130 | it('should parse request', function() { 131 | expect(out.clientID).to.equal('c123'); 132 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 133 | expect(out.scope).to.be.an('array'); 134 | expect(out.scope).to.have.length(2); 135 | expect(out.scope[0]).to.equal('read'); 136 | expect(out.scope[1]).to.equal('write'); 137 | expect(out.state).to.equal('f1o1o1'); 138 | expect(out.nonce).to.equal('n123'); 139 | }); 140 | }); 141 | 142 | describe('request with list of scopes using scope separator option', function() { 143 | var err, out; 144 | 145 | before(function(done) { 146 | chai.oauth2orize.grant(codeIdToken({ scopeSeparator: ',' }, issueCode, issueIDToken)) 147 | .req(function(req) { 148 | req.query = {}; 149 | req.query.client_id = 'c123'; 150 | req.query.redirect_uri = 'http://example.com/auth/callback'; 151 | req.query.scope = 'read,write'; 152 | req.query.state = 'f1o1o1'; 153 | req.query.nonce = 'n123'; 154 | }) 155 | .parse(function(e, o) { 156 | err = e; 157 | out = o; 158 | done(); 159 | }) 160 | .authorize(); 161 | }); 162 | 163 | it('should not error', function() { 164 | expect(err).to.be.null; 165 | }); 166 | 167 | it('should parse request', function() { 168 | expect(out.clientID).to.equal('c123'); 169 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 170 | expect(out.scope).to.be.an('array'); 171 | expect(out.scope).to.have.length(2); 172 | expect(out.scope[0]).to.equal('read'); 173 | expect(out.scope[1]).to.equal('write'); 174 | expect(out.state).to.equal('f1o1o1'); 175 | expect(out.nonce).to.equal('n123'); 176 | }); 177 | }); 178 | 179 | describe('request with list of scopes separated by space using multiple scope separator option', function() { 180 | var err, out; 181 | 182 | before(function(done) { 183 | chai.oauth2orize.grant(codeIdToken({ scopeSeparator: [' ', ','] }, issueCode, issueIDToken)) 184 | .req(function(req) { 185 | req.query = {}; 186 | req.query.client_id = 'c123'; 187 | req.query.redirect_uri = 'http://example.com/auth/callback'; 188 | req.query.scope = 'read write'; 189 | req.query.state = 'f1o1o1'; 190 | req.query.nonce = 'n123'; 191 | }) 192 | .parse(function(e, o) { 193 | err = e; 194 | out = o; 195 | done(); 196 | }) 197 | .authorize(); 198 | }); 199 | 200 | it('should not error', function() { 201 | expect(err).to.be.null; 202 | }); 203 | 204 | it('should parse request', function() { 205 | expect(out.clientID).to.equal('c123'); 206 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 207 | expect(out.scope).to.be.an('array'); 208 | expect(out.scope).to.have.length(2); 209 | expect(out.scope[0]).to.equal('read'); 210 | expect(out.scope[1]).to.equal('write'); 211 | expect(out.state).to.equal('f1o1o1'); 212 | expect(out.nonce).to.equal('n123'); 213 | }); 214 | }); 215 | 216 | describe('request with list of scopes separated by comma using multiple scope separator option', function() { 217 | var err, out; 218 | 219 | before(function(done) { 220 | chai.oauth2orize.grant(codeIdToken({ scopeSeparator: [' ', ','] }, issueCode, issueIDToken)) 221 | .req(function(req) { 222 | req.query = {}; 223 | req.query.client_id = 'c123'; 224 | req.query.redirect_uri = 'http://example.com/auth/callback'; 225 | req.query.scope = 'read,write'; 226 | req.query.state = 'f1o1o1'; 227 | req.query.nonce = 'n123'; 228 | }) 229 | .parse(function(e, o) { 230 | err = e; 231 | out = o; 232 | done(); 233 | }) 234 | .authorize(); 235 | }); 236 | 237 | it('should not error', function() { 238 | expect(err).to.be.null; 239 | }); 240 | 241 | it('should parse request', function() { 242 | expect(out.clientID).to.equal('c123'); 243 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 244 | expect(out.scope).to.be.an('array'); 245 | expect(out.scope).to.have.length(2); 246 | expect(out.scope[0]).to.equal('read'); 247 | expect(out.scope[1]).to.equal('write'); 248 | expect(out.state).to.equal('f1o1o1'); 249 | expect(out.nonce).to.equal('n123'); 250 | }); 251 | }); 252 | 253 | describe('request with missing client_id parameter', function() { 254 | var err, out; 255 | 256 | before(function(done) { 257 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 258 | .req(function(req) { 259 | req.query = {}; 260 | req.query.redirect_uri = 'http://example.com/auth/callback'; 261 | req.query.state = 'f1o1o1'; 262 | req.query.nonce = 'n123'; 263 | }) 264 | .parse(function(e, o) { 265 | err = e; 266 | out = o; 267 | done(); 268 | }) 269 | .authorize(); 270 | }); 271 | 272 | it('should error', function() { 273 | expect(err).to.be.an.instanceOf(Error); 274 | expect(err.constructor.name).to.equal('AuthorizationError'); 275 | expect(err.message).to.equal('Missing required parameter: client_id'); 276 | expect(err.code).to.equal('invalid_request'); 277 | }); 278 | }); 279 | 280 | describe('request with invalid client_id parameter', function() { 281 | var err, out; 282 | 283 | before(function(done) { 284 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 285 | .req(function(req) { 286 | req.query = {}; 287 | req.query.client_id = ['c123', 'c123']; 288 | req.query.redirect_uri = 'http://example.com/auth/callback'; 289 | req.query.state = 'f1o1o1'; 290 | req.query.nonce = 'n123'; 291 | }) 292 | .parse(function(e, o) { 293 | err = e; 294 | out = o; 295 | done(); 296 | }) 297 | .authorize(); 298 | }); 299 | 300 | it('should error', function() { 301 | expect(err).to.be.an.instanceOf(Error); 302 | expect(err.constructor.name).to.equal('AuthorizationError'); 303 | expect(err.message).to.equal('Invalid parameter: client_id must be a string'); 304 | expect(err.code).to.equal('invalid_request'); 305 | }); 306 | }); 307 | 308 | describe('with missing nonce parameter', function() { 309 | var err, out; 310 | 311 | before(function(done) { 312 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 313 | .req(function(req) { 314 | req.query = {}; 315 | req.query.client_id = 'c123'; 316 | req.query.redirect_uri = 'http://example.com/auth/callback'; 317 | req.query.state = 'f1o1o1'; 318 | }) 319 | .parse(function(e, o) { 320 | err = e; 321 | out = o; 322 | done(); 323 | }) 324 | .authorize(); 325 | }); 326 | 327 | it('should error', function() { 328 | expect(err).to.be.an.instanceOf(Error); 329 | expect(err.constructor.name).to.equal('AuthorizationError'); 330 | expect(err.message).to.equal('Missing required parameter: nonce'); 331 | expect(err.code).to.equal('invalid_request'); 332 | }); 333 | }); 334 | 335 | describe('request with invalid nonce parameter', function() { 336 | var err, out; 337 | 338 | before(function(done) { 339 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 340 | .req(function(req) { 341 | req.query = {}; 342 | req.query.client_id = 'c123'; 343 | req.query.redirect_uri = 'http://example.com/auth/callback'; 344 | req.query.state = 'f1o1o1'; 345 | req.query.nonce = ['n123', 'n123']; 346 | }) 347 | .parse(function(e, o) { 348 | err = e; 349 | out = o; 350 | done(); 351 | }) 352 | .authorize(); 353 | }); 354 | 355 | it('should error', function() { 356 | expect(err).to.be.an.instanceOf(Error); 357 | expect(err.constructor.name).to.equal('AuthorizationError'); 358 | expect(err.message).to.equal('Invalid parameter: nonce must be a string'); 359 | expect(err.code).to.equal('invalid_request'); 360 | }); 361 | }); 362 | 363 | describe('with scope parameter that is not a string', function() { 364 | var err, out; 365 | 366 | before(function(done) { 367 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 368 | .req(function(req) { 369 | req.query = {}; 370 | req.query.client_id = 'c123'; 371 | req.query.redirect_uri = 'http://example.com/auth/callback'; 372 | req.query.state = 'f1o1o1'; 373 | req.query.nonce = 'n123'; 374 | req.query.scope = ['read', 'write']; 375 | }) 376 | .parse(function(e, o) { 377 | err = e; 378 | out = o; 379 | done(); 380 | }) 381 | .authorize(); 382 | }); 383 | 384 | it('should error', function() { 385 | expect(err).to.be.an.instanceOf(Error); 386 | expect(err.constructor.name).to.equal('AuthorizationError'); 387 | expect(err.message).to.equal('Invalid parameter: scope must be a string'); 388 | expect(err.code).to.equal('invalid_request'); 389 | }); 390 | }); 391 | }); 392 | 393 | 394 | describe('decision handling', function() { 395 | 396 | describe('transaction', function() { 397 | function issueCode(client, redirectURI, user, ares, areq, locals, done) { 398 | expect(client.id).to.equal('c123'); 399 | expect(client.name).to.equal('Example'); 400 | expect(redirectURI).to.equal('http://example.com/auth/callback'); 401 | expect(user.id).to.equal('u123'); 402 | expect(user.name).to.equal('Bob'); 403 | expect(ares.allow).to.equal(true); 404 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 405 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 406 | expect(locals.foo).to.equal('bar'); 407 | 408 | return done(null, 'xyz'); 409 | } 410 | 411 | function issueIDToken(client, user, ares, areq, opts, done) { 412 | // TODO: assert over ares 413 | expect(client.id).to.equal('c123'); 414 | expect(user.id).to.equal('u123'); 415 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 416 | expect(opts.authorizationCode).to.equal('xyz'); 417 | 418 | return done(null, 'idtoken'); 419 | } 420 | 421 | 422 | var response; 423 | 424 | before(function(done) { 425 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 426 | .txn(function(txn) { 427 | txn.client = { id: 'c123', name: 'Example' }; 428 | txn.redirectURI = 'http://www.example.com/auth/callback'; 429 | txn.req = { 430 | redirectURI: 'http://example.com/auth/callback', 431 | nonce: 'n-0S6_WzA2Mj' 432 | }; 433 | txn.user = { id: 'u123', name: 'Bob' }; 434 | txn.res = { allow: true }; 435 | txn.locals = { foo: 'bar' }; 436 | }) 437 | .end(function(res) { 438 | response = res; 439 | done(); 440 | }) 441 | .decide(); 442 | }); 443 | 444 | it('should respond', function() { 445 | expect(response.statusCode).to.equal(302); 446 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#code=xyz&id_token=idtoken'); 447 | }); 448 | }); 449 | 450 | describe('transaction with request state', function() { 451 | function issueCode(client, redirectURI, user, ares, areq, locals, done) { 452 | expect(client.id).to.equal('c123'); 453 | expect(client.name).to.equal('Example'); 454 | expect(redirectURI).to.equal('http://example.com/auth/callback'); 455 | expect(user.id).to.equal('u123'); 456 | expect(user.name).to.equal('Bob'); 457 | expect(ares.allow).to.equal(true); 458 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 459 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 460 | expect(areq.state).to.equal('f1o1o1'); 461 | expect(locals.foo).to.equal('bar'); 462 | 463 | return done(null, 'xyz'); 464 | } 465 | 466 | function issueIDToken(client, user, ares, areq, opts, done) { 467 | expect(client.id).to.equal('c123'); 468 | expect(user.id).to.equal('u123'); 469 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 470 | expect(opts.authorizationCode).to.equal('xyz'); 471 | 472 | return done(null, 'idtoken'); 473 | } 474 | 475 | 476 | var response; 477 | 478 | before(function(done) { 479 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 480 | .txn(function(txn) { 481 | txn.client = { id: 'c123', name: 'Example' }; 482 | txn.redirectURI = 'http://www.example.com/auth/callback'; 483 | txn.req = { 484 | redirectURI: 'http://example.com/auth/callback', 485 | state: 'f1o1o1', 486 | nonce: 'n-0S6_WzA2Mj' 487 | }; 488 | txn.user = { id: 'u123', name: 'Bob' }; 489 | txn.res = { allow: true }; 490 | txn.locals = { foo: 'bar' }; 491 | }) 492 | .end(function(res) { 493 | response = res; 494 | done(); 495 | }) 496 | .decide(); 497 | }); 498 | 499 | it('should respond', function() { 500 | expect(response.statusCode).to.equal(302); 501 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#code=xyz&id_token=idtoken&state=f1o1o1'); 502 | }); 503 | }); 504 | 505 | describe('disallowed transaction', function() { 506 | function issueCode(client, redirectURI, user, done) { 507 | if (client.id == 'c123' && redirectURI == 'http://example.com/auth/callback' && user.id == 'u123') { 508 | return done(null, 'xyz'); 509 | } 510 | return done(new Error('something went wrong')); 511 | } 512 | 513 | function issueIDToken(client, user, areq, code, done) { 514 | return done(null, 'idtoken'); 515 | } 516 | 517 | 518 | var response; 519 | 520 | before(function(done) { 521 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 522 | .txn(function(txn) { 523 | txn.client = { id: 'c123', name: 'Example' }; 524 | txn.redirectURI = 'http://www.example.com/auth/callback'; 525 | txn.req = { 526 | redirectURI: 'http://example.com/auth/callback', 527 | nonce: 'n-0S6_WzA2Mj' 528 | }; 529 | txn.user = { id: 'u123', name: 'Bob' }; 530 | txn.res = { allow: false }; 531 | }) 532 | .end(function(res) { 533 | response = res; 534 | done(); 535 | }) 536 | .decide(); 537 | }); 538 | 539 | it('should respond', function() { 540 | expect(response.statusCode).to.equal(302); 541 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#error=access_denied'); 542 | }); 543 | }); 544 | 545 | describe('disallowed transaction with request state', function() { 546 | function issueCode(client, redirectURI, user, done) { 547 | if (client.id == 'c123' && redirectURI == 'http://example.com/auth/callback' && user.id == 'u123') { 548 | return done(null, 'xyz'); 549 | } 550 | return done(new Error('something went wrong')); 551 | } 552 | 553 | function issueIDToken(client, user, areq, code, done) { 554 | return done(null, 'idtoken'); 555 | } 556 | 557 | 558 | var response; 559 | 560 | before(function(done) { 561 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 562 | .txn(function(txn) { 563 | txn.client = { id: 'c123', name: 'Example' }; 564 | txn.redirectURI = 'http://www.example.com/auth/callback'; 565 | txn.req = { 566 | redirectURI: 'http://example.com/auth/callback', 567 | state: 'f2o2o2', 568 | nonce: 'n-0S6_WzA2Mj' 569 | }; 570 | txn.user = { id: 'u123', name: 'Bob' }; 571 | txn.res = { allow: false }; 572 | }) 573 | .end(function(res) { 574 | response = res; 575 | done(); 576 | }) 577 | .decide(); 578 | }); 579 | 580 | it('should respond', function() { 581 | expect(response.statusCode).to.equal(302); 582 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#error=access_denied&state=f2o2o2'); 583 | }); 584 | }); 585 | 586 | describe('unauthorized client', function() { 587 | function issueCode(client, redirectURI, user, done) { 588 | if (client.id == 'cUNAUTHZ') { 589 | return done(null, false); 590 | } 591 | return done(new Error('something went wrong')); 592 | } 593 | 594 | function issueIDToken(client, user, areq, code, done) { 595 | return done(null, 'idtoken'); 596 | } 597 | 598 | 599 | var err; 600 | 601 | before(function(done) { 602 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 603 | .txn(function(txn) { 604 | txn.client = { id: 'cUNAUTHZ', name: 'Example' }; 605 | txn.redirectURI = 'http://www.example.com/auth/callback'; 606 | txn.req = { 607 | redirectURI: 'http://example.com/auth/callback', 608 | nonce: 'n-0S6_WzA2Mj' 609 | }; 610 | txn.user = { id: 'u123', name: 'Bob' }; 611 | txn.res = { allow: true }; 612 | }) 613 | .next(function(e) { 614 | err = e; 615 | done(); 616 | }) 617 | .decide(); 618 | }); 619 | 620 | it('should error', function() { 621 | expect(err).to.be.an.instanceOf(Error); 622 | expect(err.constructor.name).to.equal('AuthorizationError'); 623 | expect(err.message).to.equal('Request denied by authorization server'); 624 | expect(err.code).to.equal('access_denied'); 625 | expect(err.status).to.equal(403); 626 | }); 627 | }); 628 | 629 | describe('encountering an error while issuing code', function() { 630 | function issueCode(client, redirectURI, user, done) { 631 | if (client.id == 'cUNAUTHZ') { 632 | return done(null, false); 633 | } 634 | return done(new Error('something went wrong')); 635 | } 636 | 637 | function issueIDToken(client, user, areq, code, done) { 638 | return done(null, 'idtoken'); 639 | } 640 | 641 | 642 | var err; 643 | 644 | before(function(done) { 645 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 646 | .txn(function(txn) { 647 | txn.client = { id: 'cERROR', name: 'Example' }; 648 | txn.redirectURI = 'http://www.example.com/auth/callback'; 649 | txn.req = { 650 | redirectURI: 'http://example.com/auth/callback', 651 | nonce: 'n-0S6_WzA2Mj' 652 | }; 653 | txn.user = { id: 'u123', name: 'Bob' }; 654 | txn.res = { allow: true }; 655 | }) 656 | .next(function(e) { 657 | err = e; 658 | done(); 659 | }) 660 | .decide(); 661 | }); 662 | 663 | it('should error', function() { 664 | expect(err).to.be.an.instanceOf(Error); 665 | expect(err.message).to.equal('something went wrong'); 666 | }); 667 | }); 668 | 669 | describe('throwing an error while issuing code', function() { 670 | function issueCode(client, redirectURI, user, done) { 671 | if (client.id == 'cTHROW') { 672 | throw new Error('something was thrown'); 673 | } 674 | return done(new Error('something went wrong')); 675 | } 676 | 677 | function issueIDToken(client, user, areq, code, done) { 678 | return done(null, 'idtoken'); 679 | } 680 | 681 | 682 | var err; 683 | 684 | before(function(done) { 685 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 686 | .txn(function(txn) { 687 | txn.client = { id: 'cTHROW', name: 'Example' }; 688 | txn.redirectURI = 'http://www.example.com/auth/callback'; 689 | txn.req = { 690 | redirectURI: 'http://example.com/auth/callback', 691 | nonce: 'n-0S6_WzA2Mj' 692 | }; 693 | txn.user = { id: 'u123', name: 'Bob' }; 694 | txn.res = { allow: true }; 695 | }) 696 | .next(function(e) { 697 | err = e; 698 | done(); 699 | }) 700 | .decide(); 701 | }); 702 | 703 | it('should error', function() { 704 | expect(err).to.be.an.instanceOf(Error); 705 | expect(err.message).to.equal('something was thrown'); 706 | }); 707 | }); 708 | 709 | describe('transaction without redirect URL', function() { 710 | function issueCode(client, redirectURI, user, done) { 711 | if (client.id == 'c123' && redirectURI == 'http://example.com/auth/callback' && user.id == 'u123') { 712 | return done(null, 'xyz'); 713 | } 714 | return done(new Error('something went wrong')); 715 | } 716 | 717 | function issueIDToken(client, user, areq, code, done) { 718 | return done(null, 'idtoken'); 719 | } 720 | 721 | 722 | var err; 723 | 724 | before(function(done) { 725 | chai.oauth2orize.grant(codeIdToken(issueCode, issueIDToken)) 726 | .txn(function(txn) { 727 | txn.client = { id: 'c123', name: 'Example' }; 728 | txn.req = { 729 | redirectURI: 'http://example.com/auth/callback', 730 | nonce: 'n-0S6_WzA2Mj' 731 | }; 732 | txn.user = { id: 'u123', name: 'Bob' }; 733 | txn.res = { allow: true }; 734 | }) 735 | .next(function(e) { 736 | err = e; 737 | done(); 738 | }) 739 | .decide(); 740 | }); 741 | 742 | it('should error', function() { 743 | expect(err).to.be.an.instanceOf(Error); 744 | expect(err.constructor.name).to.equal('AuthorizationError'); 745 | expect(err.code).to.equal('server_error'); 746 | expect(err.message).to.equal('Unable to issue redirect for OAuth 2.0 transaction'); 747 | }); 748 | }); 749 | }); 750 | 751 | }); -------------------------------------------------------------------------------- /test/grant/codeIdTokenToken.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , codeIdTokenToken = require('../../lib/grant/codeIdTokenToken'); 3 | 4 | 5 | describe('grant.codeIdTokenToken', function() { 6 | 7 | describe('module', function() { 8 | var mod = codeIdTokenToken(function(){}, function(){}, function(){}); 9 | 10 | it('should be named code token', function() { 11 | expect(mod.name).to.equal('code id_token token'); 12 | }); 13 | 14 | it('should expose request and response functions', function() { 15 | expect(mod.request).to.be.a('function'); 16 | expect(mod.response).to.be.a('function'); 17 | }); 18 | }); 19 | 20 | it('should throw if constructed without a issueToken callback', function() { 21 | expect(function() { 22 | codeIdTokenToken(); 23 | }).to.throw(TypeError, 'oauth2orize-openid.codeIDTokenToken grant requires an issueToken callback'); 24 | }); 25 | 26 | it('should throw if constructed without a issueCode callback', function() { 27 | expect(function() { 28 | codeIdTokenToken(function(){}); 29 | }).to.throw(TypeError, 'oauth2orize-openid.codeIDTokenToken grant requires an issueCode callback'); 30 | }); 31 | 32 | it('should throw if constructed without a issueIDToken callback', function() { 33 | expect(function() { 34 | codeIdTokenToken(function(){}, function(){}); 35 | }).to.throw(TypeError, 'oauth2orize-openid.codeIDTokenToken grant requires an issueIDToken callback'); 36 | }); 37 | 38 | describe('request parsing', function() { 39 | function issueToken(){} 40 | function issueCode(){} 41 | function issueIDToken(){} 42 | 43 | describe('request', function() { 44 | var err, out; 45 | 46 | before(function(done) { 47 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 48 | .req(function(req) { 49 | req.query = {}; 50 | req.query.client_id = 'c123'; 51 | req.query.redirect_uri = 'http://example.com/auth/callback'; 52 | req.query.state = 'f1o1o1'; 53 | req.query.nonce = 'n123'; 54 | }) 55 | .parse(function(e, o) { 56 | err = e; 57 | out = o; 58 | done(); 59 | }) 60 | .authorize(); 61 | }); 62 | 63 | it('should not error', function() { 64 | expect(err).to.be.null; 65 | }); 66 | 67 | it('should parse request', function() { 68 | expect(out.clientID).to.equal('c123'); 69 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 70 | expect(out.scope).to.be.undefined; 71 | expect(out.state).to.equal('f1o1o1'); 72 | expect(out.nonce).to.equal('n123'); 73 | }); 74 | }); 75 | 76 | describe('request with scope', function() { 77 | var err, out; 78 | 79 | before(function(done) { 80 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 81 | .req(function(req) { 82 | req.query = {}; 83 | req.query.client_id = 'c123'; 84 | req.query.redirect_uri = 'http://example.com/auth/callback'; 85 | req.query.scope = 'read'; 86 | req.query.state = 'f1o1o1'; 87 | req.query.nonce = 'n123'; 88 | }) 89 | .parse(function(e, o) { 90 | err = e; 91 | out = o; 92 | done(); 93 | }) 94 | .authorize(); 95 | }); 96 | 97 | it('should not error', function() { 98 | expect(err).to.be.null; 99 | }); 100 | 101 | it('should parse request', function() { 102 | expect(out.clientID).to.equal('c123'); 103 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 104 | expect(out.scope).to.be.an('array'); 105 | expect(out.scope).to.have.length(1); 106 | expect(out.scope[0]).to.equal('read'); 107 | expect(out.state).to.equal('f1o1o1'); 108 | expect(out.nonce).to.equal('n123'); 109 | }); 110 | }); 111 | 112 | describe('request with list of scopes', function() { 113 | var err, out; 114 | 115 | before(function(done) { 116 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 117 | .req(function(req) { 118 | req.query = {}; 119 | req.query.client_id = 'c123'; 120 | req.query.redirect_uri = 'http://example.com/auth/callback'; 121 | req.query.scope = 'read write'; 122 | req.query.state = 'f1o1o1'; 123 | req.query.nonce = 'n123'; 124 | }) 125 | .parse(function(e, o) { 126 | err = e; 127 | out = o; 128 | done(); 129 | }) 130 | .authorize(); 131 | }); 132 | 133 | it('should not error', function() { 134 | expect(err).to.be.null; 135 | }); 136 | 137 | it('should parse request', function() { 138 | expect(out.clientID).to.equal('c123'); 139 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 140 | expect(out.scope).to.be.an('array'); 141 | expect(out.scope).to.have.length(2); 142 | expect(out.scope[0]).to.equal('read'); 143 | expect(out.scope[1]).to.equal('write'); 144 | expect(out.state).to.equal('f1o1o1'); 145 | expect(out.nonce).to.equal('n123'); 146 | }); 147 | }); 148 | 149 | describe('request with list of scopes using scope separator option', function() { 150 | var err, out; 151 | 152 | before(function(done) { 153 | chai.oauth2orize.grant(codeIdTokenToken({ scopeSeparator: ',' }, issueToken, issueCode, issueIDToken)) 154 | .req(function(req) { 155 | req.query = {}; 156 | req.query.client_id = 'c123'; 157 | req.query.redirect_uri = 'http://example.com/auth/callback'; 158 | req.query.scope = 'read,write'; 159 | req.query.state = 'f1o1o1'; 160 | req.query.nonce = 'n123'; 161 | }) 162 | .parse(function(e, o) { 163 | err = e; 164 | out = o; 165 | done(); 166 | }) 167 | .authorize(); 168 | }); 169 | 170 | it('should not error', function() { 171 | expect(err).to.be.null; 172 | }); 173 | 174 | it('should parse request', function() { 175 | expect(out.clientID).to.equal('c123'); 176 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 177 | expect(out.scope).to.be.an('array'); 178 | expect(out.scope).to.have.length(2); 179 | expect(out.scope[0]).to.equal('read'); 180 | expect(out.scope[1]).to.equal('write'); 181 | expect(out.state).to.equal('f1o1o1'); 182 | expect(out.nonce).to.equal('n123'); 183 | }); 184 | }); 185 | 186 | describe('request with list of scopes separated by space using multiple scope separator option', function() { 187 | var err, out; 188 | 189 | before(function(done) { 190 | chai.oauth2orize.grant(codeIdTokenToken({ scopeSeparator: [' ', ','] }, issueToken, issueCode, issueIDToken)) 191 | .req(function(req) { 192 | req.query = {}; 193 | req.query.client_id = 'c123'; 194 | req.query.redirect_uri = 'http://example.com/auth/callback'; 195 | req.query.scope = 'read write'; 196 | req.query.state = 'f1o1o1'; 197 | req.query.nonce = 'n123'; 198 | }) 199 | .parse(function(e, o) { 200 | err = e; 201 | out = o; 202 | done(); 203 | }) 204 | .authorize(); 205 | }); 206 | 207 | it('should not error', function() { 208 | expect(err).to.be.null; 209 | }); 210 | 211 | it('should parse request', function() { 212 | expect(out.clientID).to.equal('c123'); 213 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 214 | expect(out.scope).to.be.an('array'); 215 | expect(out.scope).to.have.length(2); 216 | expect(out.scope[0]).to.equal('read'); 217 | expect(out.scope[1]).to.equal('write'); 218 | expect(out.state).to.equal('f1o1o1'); 219 | expect(out.nonce).to.equal('n123'); 220 | }); 221 | }); 222 | 223 | describe('request with list of scopes separated by comma using multiple scope separator option', function() { 224 | var err, out; 225 | 226 | before(function(done) { 227 | chai.oauth2orize.grant(codeIdTokenToken({ scopeSeparator: [' ', ','] }, issueToken, issueCode, issueIDToken)) 228 | .req(function(req) { 229 | req.query = {}; 230 | req.query.client_id = 'c123'; 231 | req.query.redirect_uri = 'http://example.com/auth/callback'; 232 | req.query.scope = 'read,write'; 233 | req.query.state = 'f1o1o1'; 234 | req.query.nonce = 'n123'; 235 | }) 236 | .parse(function(e, o) { 237 | err = e; 238 | out = o; 239 | done(); 240 | }) 241 | .authorize(); 242 | }); 243 | 244 | it('should not error', function() { 245 | expect(err).to.be.null; 246 | }); 247 | 248 | it('should parse request', function() { 249 | expect(out.clientID).to.equal('c123'); 250 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 251 | expect(out.scope).to.be.an('array'); 252 | expect(out.scope).to.have.length(2); 253 | expect(out.scope[0]).to.equal('read'); 254 | expect(out.scope[1]).to.equal('write'); 255 | expect(out.state).to.equal('f1o1o1'); 256 | expect(out.nonce).to.equal('n123'); 257 | }); 258 | }); 259 | 260 | describe('request with missing client_id parameter', function() { 261 | var err, out; 262 | 263 | before(function(done) { 264 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 265 | .req(function(req) { 266 | req.query = {}; 267 | req.query.redirect_uri = 'http://example.com/auth/callback'; 268 | req.query.state = 'f1o1o1'; 269 | req.query.nonce = 'n123'; 270 | }) 271 | .parse(function(e, o) { 272 | err = e; 273 | out = o; 274 | done(); 275 | }) 276 | .authorize(); 277 | }); 278 | 279 | it('should error', function() { 280 | expect(err).to.be.an.instanceOf(Error); 281 | expect(err.constructor.name).to.equal('AuthorizationError'); 282 | expect(err.message).to.equal('Missing required parameter: client_id'); 283 | expect(err.code).to.equal('invalid_request'); 284 | }); 285 | }); 286 | 287 | describe('request with invalid client_id parameter', function() { 288 | var err, out; 289 | 290 | before(function(done) { 291 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 292 | .req(function(req) { 293 | req.query = {}; 294 | req.query.client_id = ['c123', 'c123']; 295 | req.query.redirect_uri = 'http://example.com/auth/callback'; 296 | req.query.state = 'f1o1o1'; 297 | req.query.nonce = 'n123'; 298 | }) 299 | .parse(function(e, o) { 300 | err = e; 301 | out = o; 302 | done(); 303 | }) 304 | .authorize(); 305 | }); 306 | 307 | it('should error', function() { 308 | expect(err).to.be.an.instanceOf(Error); 309 | expect(err.constructor.name).to.equal('AuthorizationError'); 310 | expect(err.message).to.equal('Invalid parameter: client_id must be a string'); 311 | expect(err.code).to.equal('invalid_request'); 312 | }); 313 | }); 314 | 315 | describe('with missing nonce parameter', function() { 316 | var err, out; 317 | 318 | before(function(done) { 319 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 320 | .req(function(req) { 321 | req.query = {}; 322 | req.query.client_id = 'c123'; 323 | req.query.redirect_uri = 'http://example.com/auth/callback'; 324 | req.query.state = 'f1o1o1'; 325 | }) 326 | .parse(function(e, o) { 327 | err = e; 328 | out = o; 329 | done(); 330 | }) 331 | .authorize(); 332 | }); 333 | 334 | it('should error', function() { 335 | expect(err).to.be.an.instanceOf(Error); 336 | expect(err.constructor.name).to.equal('AuthorizationError'); 337 | expect(err.message).to.equal('Missing required parameter: nonce'); 338 | expect(err.code).to.equal('invalid_request'); 339 | }); 340 | }); 341 | 342 | describe('request with invalid nonce parameter', function() { 343 | var err, out; 344 | 345 | before(function(done) { 346 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 347 | .req(function(req) { 348 | req.query = {}; 349 | req.query.client_id = 'c123'; 350 | req.query.redirect_uri = 'http://example.com/auth/callback'; 351 | req.query.state = 'f1o1o1'; 352 | req.query.nonce = ['n123', 'n123']; 353 | }) 354 | .parse(function(e, o) { 355 | err = e; 356 | out = o; 357 | done(); 358 | }) 359 | .authorize(); 360 | }); 361 | 362 | it('should error', function() { 363 | expect(err).to.be.an.instanceOf(Error); 364 | expect(err.constructor.name).to.equal('AuthorizationError'); 365 | expect(err.message).to.equal('Invalid parameter: nonce must be a string'); 366 | expect(err.code).to.equal('invalid_request'); 367 | }); 368 | }); 369 | 370 | describe('with scope parameter that is not a string', function() { 371 | var err, out; 372 | 373 | before(function(done) { 374 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 375 | .req(function(req) { 376 | req.query = {}; 377 | req.query.client_id = 'c123'; 378 | req.query.redirect_uri = 'http://example.com/auth/callback'; 379 | req.query.state = 'f1o1o1'; 380 | req.query.nonce = 'n123'; 381 | req.query.scope = ['read', 'write']; 382 | }) 383 | .parse(function(e, o) { 384 | err = e; 385 | out = o; 386 | done(); 387 | }) 388 | .authorize(); 389 | }); 390 | 391 | it('should error', function() { 392 | expect(err).to.be.an.instanceOf(Error); 393 | expect(err.constructor.name).to.equal('AuthorizationError'); 394 | expect(err.message).to.equal('Invalid parameter: scope must be a string'); 395 | expect(err.code).to.equal('invalid_request'); 396 | }); 397 | }); 398 | }); 399 | 400 | describe('decision handling', function() { 401 | 402 | describe('transaction', function() { 403 | function issueToken(client, user, ares, areq, locals, done) { 404 | expect(client.id).to.equal('c123'); 405 | expect(client.name).to.equal('Example'); 406 | expect(user.id).to.equal('u123'); 407 | expect(user.name).to.equal('Bob'); 408 | expect(ares.allow).to.equal(true); 409 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 410 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 411 | expect(locals.foo).to.equal('bar'); 412 | 413 | return done(null, 'at-xyz'); 414 | } 415 | 416 | function issueCode(client, redirectURI, user, ares, areq, locals, done) { 417 | expect(client.id).to.equal('c123'); 418 | expect(client.name).to.equal('Example'); 419 | expect(redirectURI).to.equal('http://example.com/auth/callback'); 420 | expect(user.id).to.equal('u123'); 421 | expect(user.name).to.equal('Bob'); 422 | expect(ares.allow).to.equal(true); 423 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 424 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 425 | expect(locals.foo).to.equal('bar'); 426 | 427 | return done(null, 'c-123'); 428 | } 429 | 430 | function issueIDToken(client, user, ares, areq, opts, done) { 431 | expect(client.id).to.equal('c123'); 432 | expect(user.id).to.equal('u123'); 433 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 434 | expect(opts.accessToken).to.equal('at-xyz'); 435 | expect(opts.authorizationCode).to.equal('c-123'); 436 | 437 | return done(null, 'idtoken'); 438 | } 439 | 440 | 441 | var response; 442 | 443 | before(function(done) { 444 | chai.oauth2orize.grant(codeIdTokenToken(issueToken, issueCode, issueIDToken)) 445 | .txn(function(txn) { 446 | txn.client = { id: 'c123', name: 'Example' }; 447 | txn.redirectURI = 'http://www.example.com/auth/callback'; 448 | txn.req = { 449 | redirectURI: 'http://example.com/auth/callback', 450 | nonce: 'n-0S6_WzA2Mj' 451 | }; 452 | txn.user = { id: 'u123', name: 'Bob' }; 453 | txn.res = { allow: true }; 454 | txn.locals = { foo: 'bar' }; 455 | }) 456 | .end(function(res) { 457 | response = res; 458 | done(); 459 | }) 460 | .decide(); 461 | }); 462 | 463 | it('should respond', function() { 464 | expect(response.statusCode).to.equal(302); 465 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#access_token=at-xyz&token_type=Bearer&code=c-123&id_token=idtoken'); 466 | }); 467 | }); 468 | 469 | }); 470 | 471 | }); 472 | -------------------------------------------------------------------------------- /test/grant/codeToken.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , codeToken = require('../../lib/grant/codeToken'); 3 | 4 | 5 | describe('grant.codeToken', function() { 6 | 7 | describe('module', function() { 8 | var mod = codeToken(function(){}, function(){}); 9 | 10 | it('should be named code token', function() { 11 | expect(mod.name).to.equal('code token'); 12 | }); 13 | 14 | it('should expose request and response functions', function() { 15 | expect(mod.request).to.be.a('function'); 16 | expect(mod.response).to.be.a('function'); 17 | }); 18 | }); 19 | 20 | it('should throw if constructed without a issueToken callback', function() { 21 | expect(function() { 22 | codeToken(); 23 | }).to.throw(TypeError, 'oauth2orize-openid.codeToken grant requires an issueToken callback'); 24 | }); 25 | 26 | it('should throw if constructed without a issueCode callback', function() { 27 | expect(function() { 28 | codeToken(function(){}); 29 | }).to.throw(TypeError, 'oauth2orize-openid.codeToken grant requires an issueCode callback'); 30 | }); 31 | 32 | describe('request parsing', function() { 33 | function issueToken(){} 34 | function issueCode(){} 35 | 36 | describe('request', function() { 37 | var err, out; 38 | 39 | before(function(done) { 40 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 41 | .req(function(req) { 42 | req.query = {}; 43 | req.query.client_id = 'c123'; 44 | req.query.redirect_uri = 'http://example.com/auth/callback'; 45 | req.query.state = 'f1o1o1'; 46 | }) 47 | .parse(function(e, o) { 48 | err = e; 49 | out = o; 50 | done(); 51 | }) 52 | .authorize(); 53 | }); 54 | 55 | it('should not error', function() { 56 | expect(err).to.be.null; 57 | }); 58 | 59 | it('should parse request', function() { 60 | expect(out.clientID).to.equal('c123'); 61 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 62 | expect(out.scope).to.be.undefined; 63 | expect(out.state).to.equal('f1o1o1'); 64 | }); 65 | }); 66 | 67 | describe('request with scope', function() { 68 | var err, out; 69 | 70 | before(function(done) { 71 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 72 | .req(function(req) { 73 | req.query = {}; 74 | req.query.client_id = 'c123'; 75 | req.query.redirect_uri = 'http://example.com/auth/callback'; 76 | req.query.scope = 'read'; 77 | req.query.state = 'f1o1o1'; 78 | }) 79 | .parse(function(e, o) { 80 | err = e; 81 | out = o; 82 | done(); 83 | }) 84 | .authorize(); 85 | }); 86 | 87 | it('should not error', function() { 88 | expect(err).to.be.null; 89 | }); 90 | 91 | it('should parse request', function() { 92 | expect(out.clientID).to.equal('c123'); 93 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 94 | expect(out.scope).to.be.an('array'); 95 | expect(out.scope).to.have.length(1); 96 | expect(out.scope[0]).to.equal('read'); 97 | expect(out.state).to.equal('f1o1o1'); 98 | }); 99 | }); 100 | 101 | describe('request with list of scopes', function() { 102 | var err, out; 103 | 104 | before(function(done) { 105 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 106 | .req(function(req) { 107 | req.query = {}; 108 | req.query.client_id = 'c123'; 109 | req.query.redirect_uri = 'http://example.com/auth/callback'; 110 | req.query.scope = 'read write'; 111 | req.query.state = 'f1o1o1'; 112 | }) 113 | .parse(function(e, o) { 114 | err = e; 115 | out = o; 116 | done(); 117 | }) 118 | .authorize(); 119 | }); 120 | 121 | it('should not error', function() { 122 | expect(err).to.be.null; 123 | }); 124 | 125 | it('should parse request', function() { 126 | expect(out.clientID).to.equal('c123'); 127 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 128 | expect(out.scope).to.be.an('array'); 129 | expect(out.scope).to.have.length(2); 130 | expect(out.scope[0]).to.equal('read'); 131 | expect(out.scope[1]).to.equal('write'); 132 | expect(out.state).to.equal('f1o1o1'); 133 | }); 134 | }); 135 | 136 | describe('request with list of scopes using scope separator option', function() { 137 | var err, out; 138 | 139 | before(function(done) { 140 | chai.oauth2orize.grant(codeToken({ scopeSeparator: ',' }, issueToken, issueCode)) 141 | .req(function(req) { 142 | req.query = {}; 143 | req.query.client_id = 'c123'; 144 | req.query.redirect_uri = 'http://example.com/auth/callback'; 145 | req.query.scope = 'read,write'; 146 | req.query.state = 'f1o1o1'; 147 | }) 148 | .parse(function(e, o) { 149 | err = e; 150 | out = o; 151 | done(); 152 | }) 153 | .authorize(); 154 | }); 155 | 156 | it('should not error', function() { 157 | expect(err).to.be.null; 158 | }); 159 | 160 | it('should parse request', function() { 161 | expect(out.clientID).to.equal('c123'); 162 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 163 | expect(out.scope).to.be.an('array'); 164 | expect(out.scope).to.have.length(2); 165 | expect(out.scope[0]).to.equal('read'); 166 | expect(out.scope[1]).to.equal('write'); 167 | expect(out.state).to.equal('f1o1o1'); 168 | }); 169 | }); 170 | 171 | describe('request with list of scopes separated by space using multiple scope separator option', function() { 172 | var err, out; 173 | 174 | before(function(done) { 175 | chai.oauth2orize.grant(codeToken({ scopeSeparator: [' ', ','] }, issueToken, issueCode)) 176 | .req(function(req) { 177 | req.query = {}; 178 | req.query.client_id = 'c123'; 179 | req.query.redirect_uri = 'http://example.com/auth/callback'; 180 | req.query.scope = 'read write'; 181 | req.query.state = 'f1o1o1'; 182 | }) 183 | .parse(function(e, o) { 184 | err = e; 185 | out = o; 186 | done(); 187 | }) 188 | .authorize(); 189 | }); 190 | 191 | it('should not error', function() { 192 | expect(err).to.be.null; 193 | }); 194 | 195 | it('should parse request', function() { 196 | expect(out.clientID).to.equal('c123'); 197 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 198 | expect(out.scope).to.be.an('array'); 199 | expect(out.scope).to.have.length(2); 200 | expect(out.scope[0]).to.equal('read'); 201 | expect(out.scope[1]).to.equal('write'); 202 | expect(out.state).to.equal('f1o1o1'); 203 | }); 204 | }); 205 | 206 | describe('request with list of scopes separated by comma using multiple scope separator option', function() { 207 | var err, out; 208 | 209 | before(function(done) { 210 | chai.oauth2orize.grant(codeToken({ scopeSeparator: [' ', ','] }, issueToken, issueCode)) 211 | .req(function(req) { 212 | req.query = {}; 213 | req.query.client_id = 'c123'; 214 | req.query.redirect_uri = 'http://example.com/auth/callback'; 215 | req.query.scope = 'read,write'; 216 | req.query.state = 'f1o1o1'; 217 | }) 218 | .parse(function(e, o) { 219 | err = e; 220 | out = o; 221 | done(); 222 | }) 223 | .authorize(); 224 | }); 225 | 226 | it('should not error', function() { 227 | expect(err).to.be.null; 228 | }); 229 | 230 | it('should parse request', function() { 231 | expect(out.clientID).to.equal('c123'); 232 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 233 | expect(out.scope).to.be.an('array'); 234 | expect(out.scope).to.have.length(2); 235 | expect(out.scope[0]).to.equal('read'); 236 | expect(out.scope[1]).to.equal('write'); 237 | expect(out.state).to.equal('f1o1o1'); 238 | }); 239 | }); 240 | 241 | describe('request with missing client_id parameter', function() { 242 | var err, out; 243 | 244 | before(function(done) { 245 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 246 | .req(function(req) { 247 | req.query = {}; 248 | req.query.redirect_uri = 'http://example.com/auth/callback'; 249 | req.query.state = 'f1o1o1'; 250 | }) 251 | .parse(function(e, o) { 252 | err = e; 253 | out = o; 254 | done(); 255 | }) 256 | .authorize(); 257 | }); 258 | 259 | it('should error', function() { 260 | expect(err).to.be.an.instanceOf(Error); 261 | expect(err.constructor.name).to.equal('AuthorizationError'); 262 | expect(err.message).to.equal('Missing required parameter: client_id'); 263 | expect(err.code).to.equal('invalid_request'); 264 | }); 265 | }); 266 | 267 | describe('request with invalid client_id parameter', function() { 268 | var err, out; 269 | 270 | before(function(done) { 271 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 272 | .req(function(req) { 273 | req.query = {}; 274 | req.query.client_id = ['c123', 'c123']; 275 | req.query.redirect_uri = 'http://example.com/auth/callback'; 276 | req.query.state = 'f1o1o1'; 277 | }) 278 | .parse(function(e, o) { 279 | err = e; 280 | out = o; 281 | done(); 282 | }) 283 | .authorize(); 284 | }); 285 | 286 | it('should error', function() { 287 | expect(err).to.be.an.instanceOf(Error); 288 | expect(err.constructor.name).to.equal('AuthorizationError'); 289 | expect(err.message).to.equal('Invalid parameter: client_id must be a string'); 290 | expect(err.code).to.equal('invalid_request'); 291 | }); 292 | }); 293 | 294 | describe('with scope parameter that is not a string', function() { 295 | var err, out; 296 | 297 | before(function(done) { 298 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 299 | .req(function(req) { 300 | req.query = {}; 301 | req.query.client_id = 'c123'; 302 | req.query.redirect_uri = 'http://example.com/auth/callback'; 303 | req.query.state = 'f1o1o1'; 304 | req.query.scope = ['read', 'write']; 305 | }) 306 | .parse(function(e, o) { 307 | err = e; 308 | out = o; 309 | done(); 310 | }) 311 | .authorize(); 312 | }); 313 | 314 | it('should error', function() { 315 | expect(err).to.be.an.instanceOf(Error); 316 | expect(err.constructor.name).to.equal('AuthorizationError'); 317 | expect(err.message).to.equal('Invalid parameter: scope must be a string'); 318 | expect(err.code).to.equal('invalid_request'); 319 | }); 320 | }); 321 | }); 322 | 323 | describe('decision handling', function() { 324 | 325 | describe('transaction', function() { 326 | function issueToken(client, user, ares, areq, locals, done) { 327 | expect(client.id).to.equal('c123'); 328 | expect(client.name).to.equal('Example'); 329 | expect(user.id).to.equal('u123'); 330 | expect(user.name).to.equal('Bob'); 331 | expect(ares.allow).to.equal(true); 332 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 333 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 334 | expect(locals.foo).to.equal('bar'); 335 | 336 | return done(null, 'at-xyz'); 337 | } 338 | 339 | function issueCode(client, redirectURI, user, ares, areq, locals, done) { 340 | expect(client.id).to.equal('c123'); 341 | expect(client.name).to.equal('Example'); 342 | expect(redirectURI).to.equal('http://example.com/auth/callback'); 343 | expect(user.id).to.equal('u123'); 344 | expect(user.name).to.equal('Bob'); 345 | expect(ares.allow).to.equal(true); 346 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 347 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 348 | expect(locals.foo).to.equal('bar'); 349 | 350 | return done(null, 'c-123'); 351 | } 352 | 353 | 354 | var response; 355 | 356 | before(function(done) { 357 | chai.oauth2orize.grant(codeToken(issueToken, issueCode)) 358 | .txn(function(txn) { 359 | txn.client = { id: 'c123', name: 'Example' }; 360 | txn.redirectURI = 'http://www.example.com/auth/callback'; 361 | txn.req = { 362 | redirectURI: 'http://example.com/auth/callback', 363 | nonce: 'n-0S6_WzA2Mj' 364 | }; 365 | txn.user = { id: 'u123', name: 'Bob' }; 366 | txn.res = { allow: true }; 367 | txn.locals = { foo: 'bar' }; 368 | }) 369 | .end(function(res) { 370 | response = res; 371 | done(); 372 | }) 373 | .decide(); 374 | }); 375 | 376 | it('should respond', function() { 377 | expect(response.statusCode).to.equal(302); 378 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#access_token=at-xyz&token_type=Bearer&code=c-123'); 379 | }); 380 | }); 381 | 382 | }); 383 | 384 | }); 385 | -------------------------------------------------------------------------------- /test/grant/idToken.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , idToken = require('../../lib/grant/idToken'); 3 | 4 | 5 | describe('grant.idToken', function() { 6 | 7 | describe('module', function() { 8 | var mod = idToken(function(){}); 9 | 10 | it('should be named id_token', function() { 11 | expect(mod.name).to.equal('id_token'); 12 | }); 13 | 14 | it('should expose request and response functions', function() { 15 | expect(mod.request).to.be.a('function'); 16 | expect(mod.response).to.be.a('function'); 17 | }); 18 | }); 19 | 20 | it('should throw if constructed without a issue callback', function() { 21 | expect(function() { 22 | idToken(); 23 | }).to.throw(TypeError, 'oauth2orize-openid.idToken grant requires an issue callback'); 24 | }); 25 | 26 | describe('request parsing', function() { 27 | function issue(){} 28 | 29 | describe('without scope', function() { 30 | var err, out; 31 | 32 | before(function(done) { 33 | chai.oauth2orize.grant(idToken(issue)) 34 | .req(function(req) { 35 | req.query = {}; 36 | req.query.client_id = 'c123'; 37 | req.query.redirect_uri = 'http://example.com/auth/callback'; 38 | req.query.state = 'f1o1o1'; 39 | req.query.nonce = 'n123'; 40 | }) 41 | .parse(function(e, o) { 42 | err = e; 43 | out = o; 44 | done(); 45 | }) 46 | .authorize(); 47 | }); 48 | 49 | it('should not error', function() { 50 | expect(err).to.be.null; 51 | }); 52 | 53 | it('should parse request', function() { 54 | expect(out.clientID).to.equal('c123'); 55 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 56 | expect(out.scope).to.be.undefined; 57 | expect(out.state).to.equal('f1o1o1'); 58 | expect(out.nonce).to.equal('n123'); 59 | }); 60 | }); 61 | 62 | describe('with scope', function() { 63 | var err, out; 64 | 65 | before(function(done) { 66 | chai.oauth2orize.grant(idToken(issue)) 67 | .req(function(req) { 68 | req.query = {}; 69 | req.query.client_id = 'c123'; 70 | req.query.redirect_uri = 'http://example.com/auth/callback'; 71 | req.query.scope = 'read'; 72 | req.query.state = 'f1o1o1'; 73 | req.query.nonce = 'n123'; 74 | }) 75 | .parse(function(e, o) { 76 | err = e; 77 | out = o; 78 | done(); 79 | }) 80 | .authorize(); 81 | }); 82 | 83 | it('should not error', function() { 84 | expect(err).to.be.null; 85 | }); 86 | 87 | it('should parse request', function() { 88 | expect(out.clientID).to.equal('c123'); 89 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 90 | expect(out.scope).to.be.an('array'); 91 | expect(out.scope).to.have.length(1); 92 | expect(out.scope[0]).to.equal('read'); 93 | expect(out.state).to.equal('f1o1o1'); 94 | expect(out.nonce).to.equal('n123'); 95 | }); 96 | }); 97 | 98 | describe('with list of scopes', function() { 99 | var err, out; 100 | 101 | before(function(done) { 102 | chai.oauth2orize.grant(idToken(issue)) 103 | .req(function(req) { 104 | req.query = {}; 105 | req.query.client_id = 'c123'; 106 | req.query.redirect_uri = 'http://example.com/auth/callback'; 107 | req.query.scope = 'read write'; 108 | req.query.state = 'f1o1o1'; 109 | req.query.nonce = 'n123'; 110 | }) 111 | .parse(function(e, o) { 112 | err = e; 113 | out = o; 114 | done(); 115 | }) 116 | .authorize(); 117 | }); 118 | 119 | it('should not error', function() { 120 | expect(err).to.be.null; 121 | }); 122 | 123 | it('should parse request', function() { 124 | expect(out.clientID).to.equal('c123'); 125 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 126 | expect(out.scope).to.be.an('array'); 127 | expect(out.scope).to.have.length(2); 128 | expect(out.scope[0]).to.equal('read'); 129 | expect(out.scope[1]).to.equal('write'); 130 | expect(out.state).to.equal('f1o1o1'); 131 | expect(out.nonce).to.equal('n123'); 132 | }); 133 | }); 134 | 135 | describe('with list of scopes using scope separator option', function() { 136 | var err, out; 137 | 138 | before(function(done) { 139 | chai.oauth2orize.grant(idToken({ scopeSeparator: ',' }, issue)) 140 | .req(function(req) { 141 | req.query = {}; 142 | req.query.client_id = 'c123'; 143 | req.query.redirect_uri = 'http://example.com/auth/callback'; 144 | req.query.scope = 'read,write'; 145 | req.query.state = 'f1o1o1'; 146 | req.query.nonce = 'n123'; 147 | }) 148 | .parse(function(e, o) { 149 | err = e; 150 | out = o; 151 | done(); 152 | }) 153 | .authorize(); 154 | }); 155 | 156 | it('should not error', function() { 157 | expect(err).to.be.null; 158 | }); 159 | 160 | it('should parse request', function() { 161 | expect(out.clientID).to.equal('c123'); 162 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 163 | expect(out.scope).to.be.an('array'); 164 | expect(out.scope).to.have.length(2); 165 | expect(out.scope[0]).to.equal('read'); 166 | expect(out.scope[1]).to.equal('write'); 167 | expect(out.state).to.equal('f1o1o1'); 168 | expect(out.nonce).to.equal('n123'); 169 | }); 170 | }); 171 | 172 | describe('with list of scopes separated by space using multiple scope separator option', function() { 173 | var err, out; 174 | 175 | before(function(done) { 176 | chai.oauth2orize.grant(idToken({ scopeSeparator: [' ', ','] }, issue)) 177 | .req(function(req) { 178 | req.query = {}; 179 | req.query.client_id = 'c123'; 180 | req.query.redirect_uri = 'http://example.com/auth/callback'; 181 | req.query.scope = 'read write'; 182 | req.query.state = 'f1o1o1'; 183 | req.query.nonce = 'n123'; 184 | }) 185 | .parse(function(e, o) { 186 | err = e; 187 | out = o; 188 | done(); 189 | }) 190 | .authorize(); 191 | }); 192 | 193 | it('should not error', function() { 194 | expect(err).to.be.null; 195 | }); 196 | 197 | it('should parse request', function() { 198 | expect(out.clientID).to.equal('c123'); 199 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 200 | expect(out.scope).to.be.an('array'); 201 | expect(out.scope).to.have.length(2); 202 | expect(out.scope[0]).to.equal('read'); 203 | expect(out.scope[1]).to.equal('write'); 204 | expect(out.state).to.equal('f1o1o1'); 205 | expect(out.nonce).to.equal('n123'); 206 | }); 207 | }); 208 | 209 | describe('with list of scopes separated by comma using multiple scope separator option', function() { 210 | var err, out; 211 | 212 | before(function(done) { 213 | chai.oauth2orize.grant(idToken({ scopeSeparator: [' ', ','] }, issue)) 214 | .req(function(req) { 215 | req.query = {}; 216 | req.query.client_id = 'c123'; 217 | req.query.redirect_uri = 'http://example.com/auth/callback'; 218 | req.query.scope = 'read,write'; 219 | req.query.state = 'f1o1o1'; 220 | req.query.nonce = 'n123'; 221 | }) 222 | .parse(function(e, o) { 223 | err = e; 224 | out = o; 225 | done(); 226 | }) 227 | .authorize(); 228 | }); 229 | 230 | it('should not error', function() { 231 | expect(err).to.be.null; 232 | }); 233 | 234 | it('should parse request', function() { 235 | expect(out.clientID).to.equal('c123'); 236 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 237 | expect(out.scope).to.be.an('array'); 238 | expect(out.scope).to.have.length(2); 239 | expect(out.scope[0]).to.equal('read'); 240 | expect(out.scope[1]).to.equal('write'); 241 | expect(out.state).to.equal('f1o1o1'); 242 | expect(out.nonce).to.equal('n123'); 243 | }); 244 | }); 245 | 246 | describe('with missing client_id parameter', function() { 247 | var err, out; 248 | 249 | before(function(done) { 250 | chai.oauth2orize.grant(idToken(issue)) 251 | .req(function(req) { 252 | req.query = {}; 253 | req.query.redirect_uri = 'http://example.com/auth/callback'; 254 | req.query.state = 'f1o1o1'; 255 | req.query.nonce = 'n123'; 256 | }) 257 | .parse(function(e, o) { 258 | err = e; 259 | out = o; 260 | done(); 261 | }) 262 | .authorize(); 263 | }); 264 | 265 | it('should error', function() { 266 | expect(err).to.be.an.instanceOf(Error); 267 | expect(err.constructor.name).to.equal('AuthorizationError'); 268 | expect(err.message).to.equal('Missing required parameter: client_id'); 269 | expect(err.code).to.equal('invalid_request'); 270 | }); 271 | }); 272 | 273 | describe('request with invalid client_id parameter', function() { 274 | var err, out; 275 | 276 | before(function(done) { 277 | chai.oauth2orize.grant(idToken(issue)) 278 | .req(function(req) { 279 | req.query = {}; 280 | req.query.client_id = ['c123', 'c123']; 281 | req.query.redirect_uri = 'http://example.com/auth/callback'; 282 | req.query.state = 'f1o1o1'; 283 | req.query.nonce = 'n123'; 284 | }) 285 | .parse(function(e, o) { 286 | err = e; 287 | out = o; 288 | done(); 289 | }) 290 | .authorize(); 291 | }); 292 | 293 | it('should error', function() { 294 | expect(err).to.be.an.instanceOf(Error); 295 | expect(err.constructor.name).to.equal('AuthorizationError'); 296 | expect(err.message).to.equal('Invalid parameter: client_id must be a string'); 297 | expect(err.code).to.equal('invalid_request'); 298 | }); 299 | }); 300 | 301 | describe('with missing nonce parameter', function() { 302 | var err, out; 303 | 304 | before(function(done) { 305 | chai.oauth2orize.grant(idToken(issue)) 306 | .req(function(req) { 307 | req.query = {}; 308 | req.query.client_id = 'c123'; 309 | req.query.redirect_uri = 'http://example.com/auth/callback'; 310 | req.query.state = 'f1o1o1'; 311 | }) 312 | .parse(function(e, o) { 313 | err = e; 314 | out = o; 315 | done(); 316 | }) 317 | .authorize(); 318 | }); 319 | 320 | it('should error', function() { 321 | expect(err).to.be.an.instanceOf(Error); 322 | expect(err.constructor.name).to.equal('AuthorizationError'); 323 | expect(err.message).to.equal('Missing required parameter: nonce'); 324 | expect(err.code).to.equal('invalid_request'); 325 | }); 326 | }); 327 | 328 | describe('request with invalid nonce parameter', function() { 329 | var err, out; 330 | 331 | before(function(done) { 332 | chai.oauth2orize.grant(idToken(issue)) 333 | .req(function(req) { 334 | req.query = {}; 335 | req.query.client_id = 'c123'; 336 | req.query.redirect_uri = 'http://example.com/auth/callback'; 337 | req.query.state = 'f1o1o1'; 338 | req.query.nonce = ['n123', 'n123']; 339 | }) 340 | .parse(function(e, o) { 341 | err = e; 342 | out = o; 343 | done(); 344 | }) 345 | .authorize(); 346 | }); 347 | 348 | it('should error', function() { 349 | expect(err).to.be.an.instanceOf(Error); 350 | expect(err.constructor.name).to.equal('AuthorizationError'); 351 | expect(err.message).to.equal('Invalid parameter: nonce must be a string'); 352 | expect(err.code).to.equal('invalid_request'); 353 | }); 354 | }); 355 | 356 | describe('with scope parameter that is not a string', function() { 357 | var err, out; 358 | 359 | before(function(done) { 360 | chai.oauth2orize.grant(idToken(issue)) 361 | .req(function(req) { 362 | req.query = {}; 363 | req.query.client_id = 'c123'; 364 | req.query.redirect_uri = 'http://example.com/auth/callback'; 365 | req.query.state = 'f1o1o1'; 366 | req.query.scope = ['read', 'write']; 367 | req.query.nonce = 'n123'; 368 | }) 369 | .parse(function(e, o) { 370 | err = e; 371 | out = o; 372 | done(); 373 | }) 374 | .authorize(); 375 | }); 376 | 377 | it('should error', function() { 378 | expect(err).to.be.an.instanceOf(Error); 379 | expect(err.constructor.name).to.equal('AuthorizationError'); 380 | expect(err.message).to.equal('Invalid parameter: scope must be a string'); 381 | expect(err.code).to.equal('invalid_request'); 382 | }); 383 | }); 384 | }); 385 | 386 | 387 | describe('issuing an ID token', function() { 388 | 389 | describe('based on client, user, and authorization request', function() { 390 | function issueIDToken(client, user, areq, done) { 391 | expect(client.id).to.equal('c123'); 392 | expect(user.id).to.equal('u123'); 393 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 394 | 395 | return done(null, 'idtoken'); 396 | } 397 | 398 | 399 | var response; 400 | 401 | before(function(done) { 402 | chai.oauth2orize.grant(idToken(issueIDToken)) 403 | .txn(function(txn) { 404 | txn.client = { id: 'c123', name: 'Example' }; 405 | txn.redirectURI = 'http://www.example.com/auth/callback'; 406 | txn.req = { 407 | redirectURI: 'http://example.com/auth/callback', 408 | nonce: 'n-0S6_WzA2Mj' 409 | }; 410 | txn.user = { id: 'u123', name: 'Bob' }; 411 | txn.res = { allow: true }; 412 | }) 413 | .end(function(res) { 414 | response = res; 415 | done(); 416 | }) 417 | .decide(); 418 | }); 419 | 420 | it('should respond', function() { 421 | expect(response.statusCode).to.equal(302); 422 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#id_token=idtoken'); 423 | }); 424 | }); 425 | 426 | describe('based on client, user, and authorization request, while preserving state', function() { 427 | function issueIDToken(client, user, areq, done) { 428 | expect(client.id).to.equal('c123'); 429 | expect(user.id).to.equal('u123'); 430 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 431 | 432 | return done(null, 'idtoken'); 433 | } 434 | 435 | 436 | var response; 437 | 438 | before(function(done) { 439 | chai.oauth2orize.grant(idToken(issueIDToken)) 440 | .txn(function(txn) { 441 | txn.client = { id: 'c123', name: 'Example' }; 442 | txn.redirectURI = 'http://www.example.com/auth/callback'; 443 | txn.req = { 444 | redirectURI: 'http://example.com/auth/callback', 445 | nonce: 'n-0S6_WzA2Mj', 446 | state: 'f1o1o1' 447 | }; 448 | txn.user = { id: 'u123', name: 'Bob' }; 449 | txn.res = { allow: true }; 450 | }) 451 | .end(function(res) { 452 | response = res; 453 | done(); 454 | }) 455 | .decide(); 456 | }); 457 | 458 | it('should respond', function() { 459 | expect(response.statusCode).to.equal(302); 460 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#id_token=idtoken&state=f1o1o1'); 461 | }); 462 | }); 463 | 464 | describe('based on client, user, authorization response, and authorization request', function() { 465 | function issueIDToken(client, user, ares, areq, done) { 466 | expect(client.id).to.equal('c123'); 467 | expect(user.id).to.equal('u123'); 468 | expect(ares.scope[0]).to.equal('profile'); 469 | expect(ares.scope[1]).to.equal('email'); 470 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 471 | 472 | return done(null, 'idtoken'); 473 | } 474 | 475 | 476 | var response; 477 | 478 | before(function(done) { 479 | chai.oauth2orize.grant(idToken(issueIDToken)) 480 | .txn(function(txn) { 481 | txn.client = { id: 'c123', name: 'Example' }; 482 | txn.redirectURI = 'http://www.example.com/auth/callback'; 483 | txn.req = { 484 | redirectURI: 'http://example.com/auth/callback', 485 | nonce: 'n-0S6_WzA2Mj' 486 | }; 487 | txn.user = { id: 'u123', name: 'Bob' }; 488 | txn.res = { allow: true, scope: [ 'profile', 'email' ] }; 489 | }) 490 | .end(function(res) { 491 | response = res; 492 | done(); 493 | }) 494 | .decide(); 495 | }); 496 | 497 | it('should respond', function() { 498 | expect(response.statusCode).to.equal(302); 499 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#id_token=idtoken'); 500 | }); 501 | }); 502 | 503 | describe('based on client, user, authorization response, authorization request, and bound parameters', function() { 504 | function issueIDToken(client, user, ares, areq, bound, done) { 505 | expect(client.id).to.equal('c123'); 506 | expect(user.id).to.equal('u123'); 507 | expect(ares.scope[0]).to.equal('profile'); 508 | expect(ares.scope[1]).to.equal('email'); 509 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 510 | expect(bound).to.equal(undefined); 511 | 512 | return done(null, 'idtoken'); 513 | } 514 | 515 | 516 | var response; 517 | 518 | before(function(done) { 519 | chai.oauth2orize.grant(idToken(issueIDToken)) 520 | .txn(function(txn) { 521 | txn.client = { id: 'c123', name: 'Example' }; 522 | txn.redirectURI = 'http://www.example.com/auth/callback'; 523 | txn.req = { 524 | redirectURI: 'http://example.com/auth/callback', 525 | nonce: 'n-0S6_WzA2Mj' 526 | }; 527 | txn.user = { id: 'u123', name: 'Bob' }; 528 | txn.res = { allow: true, scope: [ 'profile', 'email' ] }; 529 | }) 530 | .end(function(res) { 531 | response = res; 532 | done(); 533 | }) 534 | .decide(); 535 | }); 536 | 537 | it('should respond', function() { 538 | expect(response.statusCode).to.equal(302); 539 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#id_token=idtoken'); 540 | }); 541 | }); 542 | 543 | describe('that was not approved by user', function() { 544 | function issueIDToken(client, user, areq, done) { 545 | expect(client.id).to.equal('c123'); 546 | expect(user.id).to.equal('u123'); 547 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 548 | 549 | return done(null, 'idtoken'); 550 | } 551 | 552 | 553 | var response; 554 | 555 | before(function(done) { 556 | chai.oauth2orize.grant(idToken(issueIDToken)) 557 | .txn(function(txn) { 558 | txn.client = { id: 'c123', name: 'Example' }; 559 | txn.redirectURI = 'http://www.example.com/auth/callback'; 560 | txn.req = { 561 | redirectURI: 'http://example.com/auth/callback', 562 | nonce: 'n-0S6_WzA2Mj' 563 | }; 564 | txn.user = { id: 'u123', name: 'Bob' }; 565 | txn.res = { allow: false }; 566 | }) 567 | .end(function(res) { 568 | response = res; 569 | done(); 570 | }) 571 | .decide(); 572 | }); 573 | 574 | it('should respond', function() { 575 | expect(response.statusCode).to.equal(302); 576 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#error=access_denied'); 577 | }); 578 | }); 579 | 580 | describe('that was not approved by user, while preserving state', function() { 581 | function issueIDToken(client, user, areq, done) { 582 | expect(client.id).to.equal('c123'); 583 | expect(user.id).to.equal('u123'); 584 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 585 | 586 | return done(null, 'idtoken'); 587 | } 588 | 589 | 590 | var response; 591 | 592 | before(function(done) { 593 | chai.oauth2orize.grant(idToken(issueIDToken)) 594 | .txn(function(txn) { 595 | txn.client = { id: 'c123', name: 'Example' }; 596 | txn.redirectURI = 'http://www.example.com/auth/callback'; 597 | txn.req = { 598 | redirectURI: 'http://example.com/auth/callback', 599 | nonce: 'n-0S6_WzA2Mj', 600 | state: 'f1o1o1' 601 | }; 602 | txn.user = { id: 'u123', name: 'Bob' }; 603 | txn.res = { allow: false }; 604 | }) 605 | .end(function(res) { 606 | response = res; 607 | done(); 608 | }) 609 | .decide(); 610 | }); 611 | 612 | it('should respond', function() { 613 | expect(response.statusCode).to.equal(302); 614 | expect(response.getHeader('Location')).to.equal('http://www.example.com/auth/callback#error=access_denied&state=f1o1o1'); 615 | }); 616 | }); 617 | 618 | describe('to an unauthorized client', function() { 619 | function issueIDToken(client, user, areq, done) { 620 | return done(null, false); 621 | } 622 | 623 | 624 | var err; 625 | 626 | before(function(done) { 627 | chai.oauth2orize.grant(idToken(issueIDToken)) 628 | .txn(function(txn) { 629 | txn.client = { id: 'c123', name: 'Example' }; 630 | txn.redirectURI = 'http://www.example.com/auth/callback'; 631 | txn.req = { 632 | redirectURI: 'http://example.com/auth/callback', 633 | nonce: 'n-0S6_WzA2Mj' 634 | }; 635 | txn.user = { id: 'u123', name: 'Bob' }; 636 | txn.res = { allow: true }; 637 | }) 638 | .next(function(e) { 639 | err = e; 640 | done(); 641 | }) 642 | .decide(); 643 | }); 644 | 645 | it('should error', function() { 646 | expect(err).to.be.an.instanceOf(Error); 647 | expect(err.constructor.name).to.equal('AuthorizationError'); 648 | expect(err.message).to.equal('Request denied by authorization server'); 649 | expect(err.code).to.equal('access_denied'); 650 | expect(err.status).to.equal(403); 651 | }); 652 | }); 653 | 654 | describe('to a transaction without redirect URL', function() { 655 | function issueIDToken(client, user, areq, done) { 656 | return done(null, 'idtoken'); 657 | } 658 | 659 | 660 | var err; 661 | 662 | before(function(done) { 663 | chai.oauth2orize.grant(idToken(issueIDToken)) 664 | .txn(function(txn) { 665 | txn.client = { id: 'c123', name: 'Example' }; 666 | txn.req = { 667 | redirectURI: 'http://example.com/auth/callback', 668 | nonce: 'n-0S6_WzA2Mj' 669 | }; 670 | txn.user = { id: 'u123', name: 'Bob' }; 671 | txn.res = { allow: true }; 672 | }) 673 | .next(function(e) { 674 | err = e; 675 | done(); 676 | }) 677 | .decide(); 678 | }); 679 | 680 | it('should error', function() { 681 | expect(err).to.be.an.instanceOf(Error); 682 | expect(err.constructor.name).to.equal('AuthorizationError'); 683 | expect(err.code).to.equal('server_error'); 684 | expect(err.message).to.equal('Unable to issue redirect for OAuth 2.0 transaction'); 685 | }); 686 | }); 687 | 688 | describe('encountering an error', function() { 689 | function issueIDToken(client, user, areq, done) { 690 | return done(new Error('something went wrong')); 691 | } 692 | 693 | 694 | var err; 695 | 696 | before(function(done) { 697 | chai.oauth2orize.grant(idToken(issueIDToken)) 698 | .txn(function(txn) { 699 | txn.client = { id: 'c123', name: 'Example' }; 700 | txn.redirectURI = 'http://www.example.com/auth/callback'; 701 | txn.req = { 702 | redirectURI: 'http://example.com/auth/callback', 703 | nonce: 'n-0S6_WzA2Mj' 704 | }; 705 | txn.user = { id: 'u123', name: 'Bob' }; 706 | txn.res = { allow: true }; 707 | }) 708 | .next(function(e) { 709 | err = e; 710 | done(); 711 | }) 712 | .decide(); 713 | }); 714 | 715 | it('should error', function() { 716 | expect(err).to.be.an.instanceOf(Error); 717 | expect(err.message).to.equal('something went wrong'); 718 | }); 719 | }); 720 | 721 | describe('encountering an exception', function() { 722 | function issueIDToken(client, user, areq, done) { 723 | throw new Error('something was thrown'); 724 | } 725 | 726 | 727 | var err; 728 | 729 | before(function(done) { 730 | chai.oauth2orize.grant(idToken(issueIDToken)) 731 | .txn(function(txn) { 732 | txn.client = { id: 'c123', name: 'Example' }; 733 | txn.redirectURI = 'http://www.example.com/auth/callback'; 734 | txn.req = { 735 | redirectURI: 'http://example.com/auth/callback', 736 | nonce: 'n-0S6_WzA2Mj' 737 | }; 738 | txn.user = { id: 'u123', name: 'Bob' }; 739 | txn.res = { allow: true }; 740 | }) 741 | .next(function(e) { 742 | err = e; 743 | done(); 744 | }) 745 | .decide(); 746 | }); 747 | 748 | it('should error', function() { 749 | expect(err).to.be.an.instanceOf(Error); 750 | expect(err.message).to.equal('something was thrown'); 751 | }); 752 | }); 753 | 754 | }); 755 | 756 | }); 757 | -------------------------------------------------------------------------------- /test/grant/idTokenToken.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , idTokenToken = require('../../lib/grant/idTokenToken'); 3 | 4 | 5 | describe('grant.idTokenToken', function() { 6 | 7 | describe('module', function() { 8 | var mod = idTokenToken(function(){}, function(){}); 9 | 10 | it('should be named id_token token', function() { 11 | expect(mod.name).to.equal('id_token token'); 12 | }); 13 | 14 | it('should expose request and response functions', function() { 15 | expect(mod.request).to.be.a('function'); 16 | expect(mod.response).to.be.a('function'); 17 | }); 18 | }); 19 | 20 | it('should throw if constructed without a issueToken callback', function() { 21 | expect(function() { 22 | idTokenToken(); 23 | }).to.throw(TypeError, 'oauth2orize-openid.idTokenToken grant requires an issueToken callback'); 24 | }); 25 | 26 | it('should throw if constructed without a issueIDToken callback', function() { 27 | expect(function() { 28 | idTokenToken(function(){}); 29 | }).to.throw(TypeError, 'oauth2orize-openid.idTokenToken grant requires an issueIDToken callback'); 30 | }); 31 | 32 | describe('request parsing', function() { 33 | function issueToken(){} 34 | function issueIDToken(){} 35 | 36 | describe('request', function() { 37 | var err, out; 38 | 39 | before(function(done) { 40 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 41 | .req(function(req) { 42 | req.query = {}; 43 | req.query.client_id = 'c123'; 44 | req.query.redirect_uri = 'http://example.com/auth/callback'; 45 | req.query.state = 'f1o1o1'; 46 | req.query.nonce = 'n123'; 47 | }) 48 | .parse(function(e, o) { 49 | err = e; 50 | out = o; 51 | done(); 52 | }) 53 | .authorize(); 54 | }); 55 | 56 | it('should not error', function() { 57 | expect(err).to.be.null; 58 | }); 59 | 60 | it('should parse request', function() { 61 | expect(out.clientID).to.equal('c123'); 62 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 63 | expect(out.scope).to.be.undefined; 64 | expect(out.state).to.equal('f1o1o1'); 65 | expect(out.nonce).to.equal('n123'); 66 | }); 67 | }); 68 | 69 | describe('request with scope', function() { 70 | var err, out; 71 | 72 | before(function(done) { 73 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 74 | .req(function(req) { 75 | req.query = {}; 76 | req.query.client_id = 'c123'; 77 | req.query.redirect_uri = 'http://example.com/auth/callback'; 78 | req.query.scope = 'read'; 79 | req.query.state = 'f1o1o1'; 80 | req.query.nonce = 'n123'; 81 | }) 82 | .parse(function(e, o) { 83 | err = e; 84 | out = o; 85 | done(); 86 | }) 87 | .authorize(); 88 | }); 89 | 90 | it('should not error', function() { 91 | expect(err).to.be.null; 92 | }); 93 | 94 | it('should parse request', function() { 95 | expect(out.clientID).to.equal('c123'); 96 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 97 | expect(out.scope).to.be.an('array'); 98 | expect(out.scope).to.have.length(1); 99 | expect(out.scope[0]).to.equal('read'); 100 | expect(out.state).to.equal('f1o1o1'); 101 | expect(out.nonce).to.equal('n123'); 102 | }); 103 | }); 104 | 105 | describe('request with list of scopes', function() { 106 | var err, out; 107 | 108 | before(function(done) { 109 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 110 | .req(function(req) { 111 | req.query = {}; 112 | req.query.client_id = 'c123'; 113 | req.query.redirect_uri = 'http://example.com/auth/callback'; 114 | req.query.scope = 'read write'; 115 | req.query.state = 'f1o1o1'; 116 | req.query.nonce = 'n123'; 117 | }) 118 | .parse(function(e, o) { 119 | err = e; 120 | out = o; 121 | done(); 122 | }) 123 | .authorize(); 124 | }); 125 | 126 | it('should not error', function() { 127 | expect(err).to.be.null; 128 | }); 129 | 130 | it('should parse request', function() { 131 | expect(out.clientID).to.equal('c123'); 132 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 133 | expect(out.scope).to.be.an('array'); 134 | expect(out.scope).to.have.length(2); 135 | expect(out.scope[0]).to.equal('read'); 136 | expect(out.scope[1]).to.equal('write'); 137 | expect(out.state).to.equal('f1o1o1'); 138 | expect(out.nonce).to.equal('n123'); 139 | }); 140 | }); 141 | 142 | describe('request with list of scopes using scope separator option', function() { 143 | var err, out; 144 | 145 | before(function(done) { 146 | chai.oauth2orize.grant(idTokenToken({ scopeSeparator: ',' }, issueToken, issueIDToken)) 147 | .req(function(req) { 148 | req.query = {}; 149 | req.query.client_id = 'c123'; 150 | req.query.redirect_uri = 'http://example.com/auth/callback'; 151 | req.query.scope = 'read,write'; 152 | req.query.state = 'f1o1o1'; 153 | req.query.nonce = 'n123'; 154 | }) 155 | .parse(function(e, o) { 156 | err = e; 157 | out = o; 158 | done(); 159 | }) 160 | .authorize(); 161 | }); 162 | 163 | it('should not error', function() { 164 | expect(err).to.be.null; 165 | }); 166 | 167 | it('should parse request', function() { 168 | expect(out.clientID).to.equal('c123'); 169 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 170 | expect(out.scope).to.be.an('array'); 171 | expect(out.scope).to.have.length(2); 172 | expect(out.scope[0]).to.equal('read'); 173 | expect(out.scope[1]).to.equal('write'); 174 | expect(out.state).to.equal('f1o1o1'); 175 | expect(out.nonce).to.equal('n123'); 176 | }); 177 | }); 178 | 179 | describe('request with list of scopes separated by space using multiple scope separator option', function() { 180 | var err, out; 181 | 182 | before(function(done) { 183 | chai.oauth2orize.grant(idTokenToken({ scopeSeparator: [' ', ','] }, issueToken, issueIDToken)) 184 | .req(function(req) { 185 | req.query = {}; 186 | req.query.client_id = 'c123'; 187 | req.query.redirect_uri = 'http://example.com/auth/callback'; 188 | req.query.scope = 'read write'; 189 | req.query.state = 'f1o1o1'; 190 | req.query.nonce = 'n123'; 191 | }) 192 | .parse(function(e, o) { 193 | err = e; 194 | out = o; 195 | done(); 196 | }) 197 | .authorize(); 198 | }); 199 | 200 | it('should not error', function() { 201 | expect(err).to.be.null; 202 | }); 203 | 204 | it('should parse request', function() { 205 | expect(out.clientID).to.equal('c123'); 206 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 207 | expect(out.scope).to.be.an('array'); 208 | expect(out.scope).to.have.length(2); 209 | expect(out.scope[0]).to.equal('read'); 210 | expect(out.scope[1]).to.equal('write'); 211 | expect(out.state).to.equal('f1o1o1'); 212 | expect(out.nonce).to.equal('n123'); 213 | }); 214 | }); 215 | 216 | describe('request with list of scopes separated by comma using multiple scope separator option', function() { 217 | var err, out; 218 | 219 | before(function(done) { 220 | chai.oauth2orize.grant(idTokenToken({ scopeSeparator: [' ', ','] }, issueToken, issueIDToken)) 221 | .req(function(req) { 222 | req.query = {}; 223 | req.query.client_id = 'c123'; 224 | req.query.redirect_uri = 'http://example.com/auth/callback'; 225 | req.query.scope = 'read,write'; 226 | req.query.state = 'f1o1o1'; 227 | req.query.nonce = 'n123'; 228 | }) 229 | .parse(function(e, o) { 230 | err = e; 231 | out = o; 232 | done(); 233 | }) 234 | .authorize(); 235 | }); 236 | 237 | it('should not error', function() { 238 | expect(err).to.be.null; 239 | }); 240 | 241 | it('should parse request', function() { 242 | expect(out.clientID).to.equal('c123'); 243 | expect(out.redirectURI).to.equal('http://example.com/auth/callback'); 244 | expect(out.scope).to.be.an('array'); 245 | expect(out.scope).to.have.length(2); 246 | expect(out.scope[0]).to.equal('read'); 247 | expect(out.scope[1]).to.equal('write'); 248 | expect(out.state).to.equal('f1o1o1'); 249 | expect(out.nonce).to.equal('n123'); 250 | }); 251 | }); 252 | 253 | describe('request with missing client_id parameter', function() { 254 | var err, out; 255 | 256 | before(function(done) { 257 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 258 | .req(function(req) { 259 | req.query = {}; 260 | req.query.redirect_uri = 'http://example.com/auth/callback'; 261 | req.query.state = 'f1o1o1'; 262 | req.query.nonce = 'n123'; 263 | }) 264 | .parse(function(e, o) { 265 | err = e; 266 | out = o; 267 | done(); 268 | }) 269 | .authorize(); 270 | }); 271 | 272 | it('should error', function() { 273 | expect(err).to.be.an.instanceOf(Error); 274 | expect(err.constructor.name).to.equal('AuthorizationError'); 275 | expect(err.message).to.equal('Missing required parameter: client_id'); 276 | expect(err.code).to.equal('invalid_request'); 277 | }); 278 | }); 279 | 280 | describe('request with invalid client_id parameter', function() { 281 | var err, out; 282 | 283 | before(function(done) { 284 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 285 | .req(function(req) { 286 | req.query = {}; 287 | req.query.client_id = ['c123', 'c123']; 288 | req.query.redirect_uri = 'http://example.com/auth/callback'; 289 | req.query.state = 'f1o1o1'; 290 | req.query.nonce = 'n123'; 291 | }) 292 | .parse(function(e, o) { 293 | err = e; 294 | out = o; 295 | done(); 296 | }) 297 | .authorize(); 298 | }); 299 | 300 | it('should error', function() { 301 | expect(err).to.be.an.instanceOf(Error); 302 | expect(err.constructor.name).to.equal('AuthorizationError'); 303 | expect(err.message).to.equal('Invalid parameter: client_id must be a string'); 304 | expect(err.code).to.equal('invalid_request'); 305 | }); 306 | }); 307 | 308 | describe('request with missing nonce parameter', function() { 309 | var err, out; 310 | 311 | before(function(done) { 312 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 313 | .req(function(req) { 314 | req.query = {}; 315 | req.query.client_id = 'c123'; 316 | req.query.redirect_uri = 'http://example.com/auth/callback'; 317 | req.query.state = 'f1o1o1'; 318 | }) 319 | .parse(function(e, o) { 320 | err = e; 321 | out = o; 322 | done(); 323 | }) 324 | .authorize(); 325 | }); 326 | 327 | it('should error', function() { 328 | expect(err).to.be.an.instanceOf(Error); 329 | expect(err.constructor.name).to.equal('AuthorizationError'); 330 | expect(err.message).to.equal('Missing required parameter: nonce'); 331 | expect(err.code).to.equal('invalid_request'); 332 | }); 333 | }); 334 | 335 | describe('request with invalid nonce parameter', function() { 336 | var err, out; 337 | 338 | before(function(done) { 339 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 340 | .req(function(req) { 341 | req.query = {}; 342 | req.query.client_id = 'c123'; 343 | req.query.redirect_uri = 'http://example.com/auth/callback'; 344 | req.query.state = 'f1o1o1'; 345 | req.query.nonce = ['n123', 'n123']; 346 | }) 347 | .parse(function(e, o) { 348 | err = e; 349 | out = o; 350 | done(); 351 | }) 352 | .authorize(); 353 | }); 354 | 355 | it('should error', function() { 356 | expect(err).to.be.an.instanceOf(Error); 357 | expect(err.constructor.name).to.equal('AuthorizationError'); 358 | expect(err.message).to.equal('Invalid parameter: nonce must be a string'); 359 | expect(err.code).to.equal('invalid_request'); 360 | }); 361 | }); 362 | 363 | describe('request with scope parameter that is not a string', function() { 364 | var err, out; 365 | 366 | before(function(done) { 367 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 368 | .req(function(req) { 369 | req.query = {}; 370 | req.query.client_id = 'c123'; 371 | req.query.redirect_uri = 'http://example.com/auth/callback'; 372 | req.query.scope = ['read', 'write']; 373 | req.query.state = 'f1o1o1'; 374 | req.query.nonce = 'n123'; 375 | }) 376 | .parse(function(e, o) { 377 | err = e; 378 | out = o; 379 | done(); 380 | }) 381 | .authorize(); 382 | }); 383 | 384 | it('should error', function() { 385 | expect(err).to.be.an.instanceOf(Error); 386 | expect(err.constructor.name).to.equal('AuthorizationError'); 387 | expect(err.message).to.equal('Invalid parameter: scope must be a string'); 388 | expect(err.code).to.equal('invalid_request'); 389 | }); 390 | }); 391 | }); 392 | 393 | describe('issuing an access token', function() { 394 | 395 | describe('based on client, user, and authorization response', function() { 396 | function issueToken(client, user, ares, done) { 397 | expect(client.id).to.equal('c123'); 398 | expect(user.id).to.equal('u123'); 399 | expect(ares.scope).to.equal('foo'); 400 | 401 | return done(null, 'xyz'); 402 | } 403 | 404 | function issueIDToken(client, user, areq, done) { 405 | return done(null, 'idtoken'); 406 | } 407 | 408 | var response; 409 | 410 | before(function(done) { 411 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 412 | .txn(function(txn) { 413 | txn.client = { id: 'c123', name: 'Example' }; 414 | txn.redirectURI = 'http://example.com/auth/callback'; 415 | txn.req = { 416 | redirectURI: 'http://example.com/auth/callback', 417 | nonce: 'n-0S6_WzA2Mj' 418 | }; 419 | txn.user = { id: 'u123', name: 'Bob' }; 420 | txn.res = { allow: true, scope: 'foo' }; 421 | }) 422 | .end(function(res) { 423 | response = res; 424 | done(); 425 | }) 426 | .decide(); 427 | }); 428 | 429 | it('should respond', function() { 430 | expect(response.statusCode).to.equal(302); 431 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&token_type=Bearer&id_token=idtoken'); 432 | }); 433 | }); 434 | 435 | describe('based on client, user, and authorization response, that adds additional parameters', function() { 436 | function issueToken(client, user, ares, done) { 437 | expect(client.id).to.equal('c123'); 438 | expect(user.id).to.equal('u123'); 439 | expect(ares.scope).to.equal('foo'); 440 | 441 | return done(null, 'xyz', { 'expires_in': 3600 }); 442 | } 443 | 444 | function issueIDToken(client, user, areq, done) { 445 | return done(null, 'idtoken'); 446 | } 447 | 448 | var response; 449 | 450 | before(function(done) { 451 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 452 | .txn(function(txn) { 453 | txn.client = { id: 'c123', name: 'Example' }; 454 | txn.redirectURI = 'http://example.com/auth/callback'; 455 | txn.req = { 456 | redirectURI: 'http://example.com/auth/callback', 457 | nonce: 'n-0S6_WzA2Mj' 458 | }; 459 | txn.user = { id: 'u123', name: 'Bob' }; 460 | txn.res = { allow: true, scope: 'foo' }; 461 | }) 462 | .end(function(res) { 463 | response = res; 464 | done(); 465 | }) 466 | .decide(); 467 | }); 468 | 469 | it('should respond', function() { 470 | expect(response.statusCode).to.equal(302); 471 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&expires_in=3600&token_type=Bearer&id_token=idtoken'); 472 | }); 473 | }); 474 | 475 | describe('based on client, user, and authorization response, that adds additional parameters including token_type', function() { 476 | function issueToken(client, user, ares, done) { 477 | expect(client.id).to.equal('c123'); 478 | expect(user.id).to.equal('u123'); 479 | expect(ares.scope).to.equal('foo'); 480 | 481 | return done(null, 'xyz', { 'token_type': 'foo', 'expires_in': 3600 }); 482 | } 483 | 484 | function issueIDToken(client, user, areq, done) { 485 | return done(null, 'idtoken'); 486 | } 487 | 488 | var response; 489 | 490 | before(function(done) { 491 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 492 | .txn(function(txn) { 493 | txn.client = { id: 'c123', name: 'Example' }; 494 | txn.redirectURI = 'http://example.com/auth/callback'; 495 | txn.req = { 496 | redirectURI: 'http://example.com/auth/callback', 497 | nonce: 'n-0S6_WzA2Mj' 498 | }; 499 | txn.user = { id: 'u123', name: 'Bob' }; 500 | txn.res = { allow: true, scope: 'foo' }; 501 | }) 502 | .end(function(res) { 503 | response = res; 504 | done(); 505 | }) 506 | .decide(); 507 | }); 508 | 509 | it('should respond', function() { 510 | expect(response.statusCode).to.equal(302); 511 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&token_type=foo&expires_in=3600&id_token=idtoken'); 512 | }); 513 | }); 514 | 515 | describe('based on client, user, and authorization response, while preserving state', function() { 516 | function issueToken(client, user, ares, done) { 517 | expect(client.id).to.equal('c123'); 518 | expect(user.id).to.equal('u123'); 519 | expect(ares.scope).to.equal('foo'); 520 | 521 | return done(null, 'xyz'); 522 | } 523 | 524 | function issueIDToken(client, user, areq, done) { 525 | return done(null, 'idtoken'); 526 | } 527 | 528 | var response; 529 | 530 | before(function(done) { 531 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 532 | .txn(function(txn) { 533 | txn.client = { id: 'c123', name: 'Example' }; 534 | txn.redirectURI = 'http://example.com/auth/callback'; 535 | txn.req = { 536 | redirectURI: 'http://example.com/auth/callback', 537 | nonce: 'n-0S6_WzA2Mj', 538 | state: 'f1o1o1' 539 | }; 540 | txn.user = { id: 'u123', name: 'Bob' }; 541 | txn.res = { allow: true, scope: 'foo' }; 542 | }) 543 | .end(function(res) { 544 | response = res; 545 | done(); 546 | }) 547 | .decide(); 548 | }); 549 | 550 | it('should respond', function() { 551 | expect(response.statusCode).to.equal(302); 552 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&token_type=Bearer&state=f1o1o1&id_token=idtoken'); 553 | }); 554 | }); 555 | 556 | describe('based on client, user, authorization request, authorization response, and transaction locals', function() { 557 | function issueToken(client, user, ares, areq, locals, done) { 558 | expect(client.id).to.equal('c123'); 559 | expect(client.name).to.equal('Example'); 560 | expect(user.id).to.equal('u123'); 561 | expect(user.name).to.equal('Bob'); 562 | expect(ares.allow).to.equal(true); 563 | expect(areq.redirectURI).to.equal('http://example.com/auth/callback'); 564 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 565 | expect(locals.foo).to.equal('bar'); 566 | 567 | return done(null, 'xyz'); 568 | } 569 | 570 | function issueIDToken(client, user, ares, areq, opts, done) { 571 | return done(null, 'idtoken'); 572 | } 573 | 574 | 575 | var response; 576 | 577 | before(function(done) { 578 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 579 | .txn(function(txn) { 580 | txn.client = { id: 'c123', name: 'Example' }; 581 | txn.redirectURI = 'http://example.com/auth/callback'; 582 | txn.req = { 583 | redirectURI: 'http://example.com/auth/callback', 584 | nonce: 'n-0S6_WzA2Mj' 585 | }; 586 | txn.user = { id: 'u123', name: 'Bob' }; 587 | txn.res = { allow: true }; 588 | txn.locals = { foo: 'bar' }; 589 | }) 590 | .end(function(res) { 591 | response = res; 592 | done(); 593 | }) 594 | .decide(); 595 | }); 596 | 597 | it('should respond', function() { 598 | expect(response.statusCode).to.equal(302); 599 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&token_type=Bearer&id_token=idtoken'); 600 | }); 601 | }); 602 | 603 | }); 604 | 605 | describe('issuing an ID token', function() { 606 | 607 | describe('based on client, user, and authorization request', function() { 608 | function issueToken(client, user, ares, done) { 609 | return done(null, 'xyz'); 610 | } 611 | 612 | function issueIDToken(client, user, areq, done) { 613 | expect(client.id).to.equal('c123'); 614 | expect(user.id).to.equal('u123'); 615 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 616 | 617 | return done(null, 'idtoken'); 618 | } 619 | 620 | 621 | var response; 622 | 623 | before(function(done) { 624 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 625 | .txn(function(txn) { 626 | txn.client = { id: 'c123', name: 'Example' }; 627 | txn.redirectURI = 'http://example.com/auth/callback'; 628 | txn.req = { 629 | redirectURI: 'http://example.com/auth/callback', 630 | nonce: 'n-0S6_WzA2Mj' 631 | }; 632 | txn.user = { id: 'u123', name: 'Bob' }; 633 | txn.res = { allow: true }; 634 | txn.locals = { foo: 'bar' }; 635 | }) 636 | .end(function(res) { 637 | response = res; 638 | done(); 639 | }) 640 | .decide(); 641 | }); 642 | 643 | it('should respond', function() { 644 | expect(response.statusCode).to.equal(302); 645 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&token_type=Bearer&id_token=idtoken'); 646 | }); 647 | }); 648 | 649 | describe('based on client, user, authorization response, authorization request, and bound parameters', function() { 650 | function issueToken(client, user, ares, done) { 651 | return done(null, 'xyz'); 652 | } 653 | 654 | function issueIDToken(client, user, ares, areq, bound, done) { 655 | expect(client.id).to.equal('c123'); 656 | expect(user.id).to.equal('u123'); 657 | expect(ares.allow).to.equal(true); 658 | expect(areq.nonce).to.equal('n-0S6_WzA2Mj'); 659 | expect(bound.accessToken).to.equal('xyz'); 660 | 661 | return done(null, 'idtoken'); 662 | } 663 | 664 | 665 | var response; 666 | 667 | before(function(done) { 668 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 669 | .txn(function(txn) { 670 | txn.client = { id: 'c123', name: 'Example' }; 671 | txn.redirectURI = 'http://example.com/auth/callback'; 672 | txn.req = { 673 | redirectURI: 'http://example.com/auth/callback', 674 | nonce: 'n-0S6_WzA2Mj' 675 | }; 676 | txn.user = { id: 'u123', name: 'Bob' }; 677 | txn.res = { allow: true }; 678 | txn.locals = { foo: 'bar' }; 679 | }) 680 | .end(function(res) { 681 | response = res; 682 | done(); 683 | }) 684 | .decide(); 685 | }); 686 | 687 | it('should respond', function() { 688 | expect(response.statusCode).to.equal(302); 689 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#access_token=xyz&token_type=Bearer&id_token=idtoken'); 690 | }); 691 | }); 692 | 693 | }); 694 | 695 | describe('generic responses', function() { 696 | 697 | describe('that was not approved by user', function() { 698 | function issueToken(client, user, ares, done) { 699 | } 700 | 701 | function issueIDToken(client, user, areq, done) { 702 | } 703 | 704 | var response; 705 | 706 | before(function(done) { 707 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 708 | .txn(function(txn) { 709 | txn.client = { id: 'c123', name: 'Example' }; 710 | txn.redirectURI = 'http://example.com/auth/callback'; 711 | txn.req = { 712 | redirectURI: 'http://example.com/auth/callback', 713 | nonce: 'n-0S6_WzA2Mj' 714 | }; 715 | txn.user = { id: 'u123', name: 'Bob' }; 716 | txn.res = { allow: false }; 717 | }) 718 | .end(function(res) { 719 | response = res; 720 | done(); 721 | }) 722 | .decide(); 723 | }); 724 | 725 | it('should respond', function() { 726 | expect(response.statusCode).to.equal(302); 727 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#error=access_denied'); 728 | }); 729 | }); 730 | 731 | describe('that was not approved by user, while preserving state', function() { 732 | function issueToken(client, user, ares, done) { 733 | } 734 | 735 | function issueIDToken(client, user, areq, done) { 736 | } 737 | 738 | var response; 739 | 740 | before(function(done) { 741 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 742 | .txn(function(txn) { 743 | txn.client = { id: 'c123', name: 'Example' }; 744 | txn.redirectURI = 'http://example.com/auth/callback'; 745 | txn.req = { 746 | redirectURI: 'http://example.com/auth/callback', 747 | nonce: 'n-0S6_WzA2Mj', 748 | state: 'f2o2o2' 749 | }; 750 | txn.user = { id: 'u123', name: 'Bob' }; 751 | txn.res = { allow: false }; 752 | }) 753 | .end(function(res) { 754 | response = res; 755 | done(); 756 | }) 757 | .decide(); 758 | }); 759 | 760 | it('should respond', function() { 761 | expect(response.statusCode).to.equal(302); 762 | expect(response.getHeader('Location')).to.equal('http://example.com/auth/callback#error=access_denied&state=f2o2o2'); 763 | }); 764 | }); 765 | 766 | }); 767 | 768 | 769 | describe('decision handling', function() { 770 | 771 | describe('unauthorized client', function() { 772 | function issueToken(client, user, done) { 773 | if (client.id == 'cUNAUTHZ') { 774 | return done(null, false); 775 | } 776 | return done(new Error('something is wrong')); 777 | } 778 | 779 | function issueIDToken(client, user, areq, accessToken, done) { 780 | return done(null, 'idtoken'); 781 | } 782 | 783 | 784 | var err; 785 | 786 | before(function(done) { 787 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 788 | .txn(function(txn) { 789 | txn.client = { id: 'cUNAUTHZ', name: 'Example' }; 790 | txn.redirectURI = 'http://example.com/auth/callback'; 791 | txn.req = { 792 | redirectURI: 'http://example.com/auth/callback' 793 | }; 794 | txn.user = { id: 'u123', name: 'Bob' }; 795 | txn.res = { allow: true }; 796 | }) 797 | .next(function(e) { 798 | err = e; 799 | done(); 800 | }) 801 | .decide(); 802 | }); 803 | 804 | it('should error', function() { 805 | expect(err).to.be.an.instanceOf(Error); 806 | expect(err.constructor.name).to.equal('AuthorizationError'); 807 | expect(err.message).to.equal('Request denied by authorization server'); 808 | expect(err.code).to.equal('access_denied'); 809 | expect(err.status).to.equal(403); 810 | }); 811 | }); 812 | 813 | describe('encountering an error while issuing token', function() { 814 | function issueToken(client, user, done) { 815 | if (client.id == 'cUNAUTHZ') { 816 | return done(null, false); 817 | } 818 | return done(new Error('something is wrong')); 819 | } 820 | 821 | function issueIDToken(client, user, areq, accessToken, done) { 822 | return done(null, 'idtoken'); 823 | } 824 | 825 | 826 | var err; 827 | 828 | before(function(done) { 829 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 830 | .txn(function(txn) { 831 | txn.client = { id: 'cERROR', name: 'Example' }; 832 | txn.redirectURI = 'http://example.com/auth/callback'; 833 | txn.req = { 834 | redirectURI: 'http://example.com/auth/callback' 835 | }; 836 | txn.user = { id: 'u123', name: 'Bob' }; 837 | txn.res = { allow: true }; 838 | }) 839 | .next(function(e) { 840 | err = e; 841 | done(); 842 | }) 843 | .decide(); 844 | }); 845 | 846 | it('should error', function() { 847 | expect(err).to.be.an.instanceOf(Error); 848 | expect(err.message).to.equal('something is wrong'); 849 | }); 850 | }); 851 | 852 | describe('throwing an error while issuing token', function() { 853 | function issueToken(client, user, done) { 854 | if (client.id == 'cTHROW') { 855 | throw new Error('something was thrown'); 856 | } 857 | return done(new Error('something is wrong')); 858 | } 859 | 860 | function issueIDToken(client, user, areq, accessToken, done) { 861 | return done(null, 'idtoken'); 862 | } 863 | 864 | var err; 865 | 866 | before(function(done) { 867 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 868 | .txn(function(txn) { 869 | txn.client = { id: 'cTHROW', name: 'Example' }; 870 | txn.redirectURI = 'http://example.com/auth/callback'; 871 | txn.req = { 872 | redirectURI: 'http://example.com/auth/callback' 873 | }; 874 | txn.user = { id: 'u123', name: 'Bob' }; 875 | txn.res = { allow: true }; 876 | }) 877 | .next(function(e) { 878 | err = e; 879 | done(); 880 | }) 881 | .decide(); 882 | }); 883 | 884 | it('should error', function() { 885 | expect(err).to.be.an.instanceOf(Error); 886 | expect(err.message).to.equal('something was thrown'); 887 | }); 888 | }); 889 | 890 | describe('transaction without redirect URL', function() { 891 | function issueToken(client, user, done) { 892 | if (client.id == 'c123' && user.id == 'u123') { 893 | return done(null, 'xyz'); 894 | } 895 | return done(new Error('something is wrong')); 896 | } 897 | 898 | function issueIDToken(client, user, areq, accessToken, done) { 899 | return done(null, 'idtoken'); 900 | } 901 | 902 | 903 | var err; 904 | 905 | before(function(done) { 906 | chai.oauth2orize.grant(idTokenToken(issueToken, issueIDToken)) 907 | .txn(function(txn) { 908 | txn.client = { id: 'c123', name: 'Example' }; 909 | txn.req = { 910 | redirectURI: 'http://example.com/auth/callback' 911 | }; 912 | txn.user = { id: 'u123', name: 'Bob' }; 913 | txn.res = { allow: true }; 914 | }) 915 | .next(function(e) { 916 | err = e; 917 | done(); 918 | }) 919 | .decide(); 920 | }); 921 | 922 | it('should error', function() { 923 | expect(err).to.be.an.instanceOf(Error); 924 | expect(err.constructor.name).to.equal('AuthorizationError'); 925 | expect(err.code).to.equal('server_error'); 926 | expect(err.message).to.equal('Unable to issue redirect for OAuth 2.0 transaction'); 927 | }); 928 | }); 929 | }); 930 | 931 | }); 932 | -------------------------------------------------------------------------------- /test/package.test.js: -------------------------------------------------------------------------------- 1 | var openid = require('..'); 2 | 3 | 4 | describe('oauth2orize-openid', function() { 5 | 6 | it('should export extensions', function() { 7 | expect(openid.extensions).to.be.a('function'); 8 | }); 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /test/request/extensions.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , extensions = require('../../lib/request/extensions') 3 | , qs = require('querystring') 4 | 5 | 6 | describe('authorization request extensions', function() { 7 | 8 | describe('module', function() { 9 | var mod = extensions(); 10 | 11 | it('should be wildcard', function() { 12 | expect(mod.name).to.equal('*'); 13 | }); 14 | 15 | it('should expose request and response functions', function() { 16 | expect(mod.request).to.be.a('function'); 17 | expect(mod.response).to.be.undefined; 18 | }); 19 | }); 20 | 21 | describe('request parsing', function() { 22 | 23 | describe('request with all parameters', function() { 24 | var err, ext; 25 | 26 | before(function(done) { 27 | chai.oauth2orize.grant(extensions()) 28 | .req(function(req) { 29 | req.query = {}; 30 | req.query.nonce = 'a1b2c3'; 31 | req.query.display = 'touch'; 32 | req.query.prompt = 'none'; 33 | req.query.max_age = '600'; 34 | req.query.ui_locales = 'en-US'; 35 | req.query.claims_locales = 'en'; 36 | req.query.id_token_hint = 'HEADER.PAYLOAD.SIGNATURE'; 37 | req.query.login_hint = 'bob@example.com'; 38 | req.query.acr_values = '0'; 39 | }) 40 | .parse(function(e, o) { 41 | err = e; 42 | ext = o; 43 | done(); 44 | }) 45 | .authorize(); 46 | }); 47 | 48 | it('should not error', function() { 49 | expect(err).to.be.null; 50 | }); 51 | 52 | it('should parse request', function() { 53 | expect(ext.nonce).to.equal('a1b2c3'); 54 | expect(ext.display).to.equal('touch'); 55 | expect(ext.prompt).to.be.an('array'); 56 | expect(ext.prompt).to.have.length(1); 57 | expect(ext.prompt[0]).to.equal('none'); 58 | expect(ext.maxAge).to.equal(600); 59 | expect(ext.uiLocales).to.be.an('array'); 60 | expect(ext.uiLocales).to.have.length(1); 61 | expect(ext.uiLocales[0]).to.equal('en-US'); 62 | expect(ext.claimsLocales).to.be.an('array'); 63 | expect(ext.claimsLocales).to.have.length(1); 64 | expect(ext.claimsLocales[0]).to.equal('en'); 65 | expect(ext.idTokenHint).to.equal('HEADER.PAYLOAD.SIGNATURE'); 66 | expect(ext.loginHint).to.equal('bob@example.com'); 67 | expect(ext.acrValues).to.be.an('array'); 68 | expect(ext.acrValues).to.have.length(1); 69 | expect(ext.acrValues[0]).to.equal('0'); 70 | }); 71 | }); 72 | 73 | describe('request without parameters', function() { 74 | var err, ext; 75 | 76 | before(function(done) { 77 | chai.oauth2orize.grant(extensions()) 78 | .req(function(req) { 79 | req.query = {}; 80 | }) 81 | .parse(function(e, o) { 82 | err = e; 83 | ext = o; 84 | done(); 85 | }) 86 | .authorize(); 87 | }); 88 | 89 | it('should not error', function() { 90 | expect(err).to.be.null; 91 | }); 92 | 93 | it('should parse request', function() { 94 | expect(ext.nonce).to.be.undefined; 95 | expect(ext.display).to.equal('page'); 96 | expect(ext.prompt).to.be.undefined; 97 | expect(ext.maxAge).to.be.undefined; 98 | expect(ext.uiLocales).to.be.undefined; 99 | expect(ext.claimsLocales).to.be.undefined; 100 | expect(ext.idTokenHint).to.be.undefined; 101 | expect(ext.loginHint).to.be.undefined; 102 | expect(ext.acrValues).to.be.undefined; 103 | expect(ext.claims).to.be.undefined; 104 | }); 105 | }); 106 | 107 | describe('request with multiple prompts', function() { 108 | var err, ext; 109 | 110 | before(function(done) { 111 | chai.oauth2orize.grant(extensions()) 112 | .req(function(req) { 113 | req.query = {}; 114 | req.query.prompt = 'login consent'; 115 | }) 116 | .parse(function(e, o) { 117 | err = e; 118 | ext = o; 119 | done(); 120 | }) 121 | .authorize(); 122 | }); 123 | 124 | it('should not error', function() { 125 | expect(err).to.be.null; 126 | }); 127 | 128 | it('should parse request', function() { 129 | expect(ext.prompt).to.be.an('array'); 130 | expect(ext.prompt).to.have.length(2); 131 | expect(ext.prompt[0]).to.equal('login'); 132 | expect(ext.prompt[1]).to.equal('consent'); 133 | }); 134 | }); 135 | 136 | describe('request with multiple UI locales', function() { 137 | var err, ext; 138 | 139 | before(function(done) { 140 | chai.oauth2orize.grant(extensions()) 141 | .req(function(req) { 142 | req.query = {}; 143 | req.query.ui_locales = 'en es'; 144 | }) 145 | .parse(function(e, o) { 146 | err = e; 147 | ext = o; 148 | done(); 149 | }) 150 | .authorize(); 151 | }); 152 | 153 | it('should not error', function() { 154 | expect(err).to.be.null; 155 | }); 156 | 157 | it('should parse request', function() { 158 | expect(ext.uiLocales).to.be.an('array'); 159 | expect(ext.uiLocales).to.have.length(2); 160 | expect(ext.uiLocales[0]).to.equal('en'); 161 | expect(ext.uiLocales[1]).to.equal('es'); 162 | }); 163 | }); 164 | 165 | describe('request with multiple claims locales', function() { 166 | var err, ext; 167 | 168 | before(function(done) { 169 | chai.oauth2orize.grant(extensions()) 170 | .req(function(req) { 171 | req.query = {}; 172 | req.query.claims_locales = 'en es'; 173 | }) 174 | .parse(function(e, o) { 175 | err = e; 176 | ext = o; 177 | done(); 178 | }) 179 | .authorize(); 180 | }); 181 | 182 | it('should not error', function() { 183 | expect(err).to.be.null; 184 | }); 185 | 186 | it('should parse request', function() { 187 | expect(ext.claimsLocales).to.be.an('array'); 188 | expect(ext.claimsLocales).to.have.length(2); 189 | expect(ext.claimsLocales[0]).to.equal('en'); 190 | expect(ext.claimsLocales[1]).to.equal('es'); 191 | }); 192 | }); 193 | 194 | describe('request with multiple ACR values', function() { 195 | var err, ext; 196 | 197 | before(function(done) { 198 | chai.oauth2orize.grant(extensions()) 199 | .req(function(req) { 200 | req.query = {}; 201 | req.query.acr_values = '2 1'; 202 | }) 203 | .parse(function(e, o) { 204 | err = e; 205 | ext = o; 206 | done(); 207 | }) 208 | .authorize(); 209 | }); 210 | 211 | it('should not error', function() { 212 | expect(err).to.be.null; 213 | }); 214 | 215 | it('should parse request', function() { 216 | expect(ext.acrValues).to.be.an('array'); 217 | expect(ext.acrValues).to.have.length(2); 218 | expect(ext.acrValues[0]).to.equal('2'); 219 | expect(ext.acrValues[1]).to.equal('1'); 220 | }); 221 | }); 222 | 223 | describe('request with claims', function() { 224 | var err, ext; 225 | 226 | before(function(done) { 227 | chai.oauth2orize.grant(extensions()) 228 | .req(function(req) { 229 | // http://lists.openid.net/pipermail/openid-specs-mobile-profile/Week-of-Mon-20141124/000070.html 230 | req.query = qs.parse('response_type=code&client_id=ABCDEFABCDEFABCDEFABCDEF&scope=openid&redirect_uri=https%3A%2F%2Femail.t-online.de%2F%3Fpf%3D%2Fem&claims=%7B%0A++%22id_token%22%3A%0A++%7B%0A+++%22email%22%3A+%7B%22essential%22%3A+true%7D%0A++%7D%0A%7D') 231 | }) 232 | .parse(function(e, o) { 233 | err = e; 234 | ext = o; 235 | done(); 236 | }) 237 | .authorize(); 238 | }); 239 | 240 | it('should not error', function() { 241 | expect(err).to.be.null; 242 | }); 243 | 244 | it('should parse request', function() { 245 | expect(ext.claims).to.be.an('object'); 246 | expect(ext.claims.id_token).to.be.an('object'); 247 | expect(ext.claims.id_token.email).to.be.an('object'); 248 | expect(ext.claims.id_token.email.essential).to.equal(true); 249 | }); 250 | }); 251 | 252 | describe('request with claims that fail to parse as JSON', function() { 253 | var err, ext; 254 | 255 | before(function(done) { 256 | chai.oauth2orize.grant(extensions()) 257 | .req(function(req) { 258 | // http://lists.openid.net/pipermail/openid-specs-mobile-profile/Week-of-Mon-20141124/000070.html 259 | req.query = {}; 260 | req.query.claims = 'xyz'; 261 | }) 262 | .parse(function(e, o) { 263 | err = e; 264 | ext = o; 265 | done(); 266 | }) 267 | .authorize(); 268 | }); 269 | 270 | it('should throw error', function() { 271 | expect(err).to.be.an.instanceOf(Error); 272 | expect(err.constructor.name).to.equal('AuthorizationError'); 273 | expect(err.message).to.equal('Failed to parse claims as JSON'); 274 | }); 275 | }); 276 | 277 | describe('request with registration', function() { 278 | var err, ext; 279 | 280 | before(function(done) { 281 | chai.oauth2orize.grant(extensions()) 282 | .req(function(req) { 283 | req.query = qs.parse('response_type=id_token&client_id=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj®istration=%7B%22logo_uri%22%3A%22https%3A%2F%2Fclient.example.org%2Flogo.png%22%7D') 284 | }) 285 | .parse(function(e, o) { 286 | err = e; 287 | ext = o; 288 | done(); 289 | }) 290 | .authorize(); 291 | }); 292 | 293 | it('should not error', function() { 294 | expect(err).to.be.null; 295 | }); 296 | 297 | it('should parse request', function() { 298 | expect(ext.registration).to.be.an('object'); 299 | expect(ext.registration.logo_uri).to.equal('https://client.example.org/logo.png'); 300 | }); 301 | }); 302 | 303 | describe('request with registration that fails to parse as JSON', function() { 304 | var err, ext; 305 | 306 | before(function(done) { 307 | chai.oauth2orize.grant(extensions()) 308 | .req(function(req) { 309 | req.query = {}; 310 | req.query.registration = 'xyz'; 311 | }) 312 | .parse(function(e, o) { 313 | err = e; 314 | ext = o; 315 | done(); 316 | }) 317 | .authorize(); 318 | }); 319 | 320 | it('should throw error', function() { 321 | expect(err).to.be.an.instanceOf(Error); 322 | expect(err.constructor.name).to.equal('AuthorizationError'); 323 | expect(err.message).to.equal('Failed to parse registration as JSON'); 324 | }); 325 | }); 326 | 327 | describe('request with nonce of wrong type', function() { 328 | var err, ext; 329 | 330 | before(function(done) { 331 | chai.oauth2orize.grant(extensions()) 332 | .req(function(req) { 333 | req.query = {}; 334 | req.query.nonce = ['uvw', 'xyz']; 335 | }) 336 | .parse(function(e, o) { 337 | err = e; 338 | ext = o; 339 | done(); 340 | }) 341 | .authorize(); 342 | }); 343 | 344 | it('should throw error', function() { 345 | expect(err).to.be.an.instanceOf(Error); 346 | expect(err.constructor.name).to.equal('AuthorizationError'); 347 | expect(err.message).to.equal('Failed to parse nonce as string'); 348 | }); 349 | }); 350 | 351 | describe('request with display of wrong type', function() { 352 | var err, ext; 353 | 354 | before(function(done) { 355 | chai.oauth2orize.grant(extensions()) 356 | .req(function(req) { 357 | req.query = {}; 358 | req.query.display = ['uvw', 'xyz']; 359 | }) 360 | .parse(function(e, o) { 361 | err = e; 362 | ext = o; 363 | done(); 364 | }) 365 | .authorize(); 366 | }); 367 | 368 | it('should throw error', function() { 369 | expect(err).to.be.an.instanceOf(Error); 370 | expect(err.constructor.name).to.equal('AuthorizationError'); 371 | expect(err.message).to.equal('Failed to parse display as string'); 372 | }); 373 | }); 374 | 375 | describe('request with prompt of wrong type', function() { 376 | var err, ext; 377 | 378 | before(function(done) { 379 | chai.oauth2orize.grant(extensions()) 380 | .req(function(req) { 381 | req.query = {}; 382 | req.query.prompt = ['uvw', 'xyz']; 383 | }) 384 | .parse(function(e, o) { 385 | err = e; 386 | ext = o; 387 | done(); 388 | }) 389 | .authorize(); 390 | }); 391 | 392 | it('should throw error', function() { 393 | expect(err).to.be.an.instanceOf(Error); 394 | expect(err.constructor.name).to.equal('AuthorizationError'); 395 | expect(err.message).to.equal('Failed to parse prompt as string'); 396 | }); 397 | }); 398 | 399 | describe('request with ui_locales of wrong type', function() { 400 | var err, ext; 401 | 402 | before(function(done) { 403 | chai.oauth2orize.grant(extensions()) 404 | .req(function(req) { 405 | req.query = {}; 406 | req.query.ui_locales = ['uvw', 'xyz']; 407 | }) 408 | .parse(function(e, o) { 409 | err = e; 410 | ext = o; 411 | done(); 412 | }) 413 | .authorize(); 414 | }); 415 | 416 | it('should throw error', function() { 417 | expect(err).to.be.an.instanceOf(Error); 418 | expect(err.constructor.name).to.equal('AuthorizationError'); 419 | expect(err.message).to.equal('Failed to parse ui_locales as string'); 420 | }); 421 | }); 422 | 423 | describe('request with claims_locales of wrong type', function() { 424 | var err, ext; 425 | 426 | before(function(done) { 427 | chai.oauth2orize.grant(extensions()) 428 | .req(function(req) { 429 | req.query = {}; 430 | req.query.claims_locales = ['uvw', 'xyz']; 431 | }) 432 | .parse(function(e, o) { 433 | err = e; 434 | ext = o; 435 | done(); 436 | }) 437 | .authorize(); 438 | }); 439 | 440 | it('should throw error', function() { 441 | expect(err).to.be.an.instanceOf(Error); 442 | expect(err.constructor.name).to.equal('AuthorizationError'); 443 | expect(err.message).to.equal('Failed to parse claims_locales as string'); 444 | }); 445 | }); 446 | 447 | describe('request with acr_values of wrong type', function() { 448 | var err, ext; 449 | 450 | before(function(done) { 451 | chai.oauth2orize.grant(extensions()) 452 | .req(function(req) { 453 | req.query = {}; 454 | req.query.acr_values = ['uvw', 'xyz']; 455 | }) 456 | .parse(function(e, o) { 457 | err = e; 458 | ext = o; 459 | done(); 460 | }) 461 | .authorize(); 462 | }); 463 | 464 | it('should throw error', function() { 465 | expect(err).to.be.an.instanceOf(Error); 466 | expect(err.constructor.name).to.equal('AuthorizationError'); 467 | expect(err.message).to.equal('Failed to parse acr_values as string'); 468 | }); 469 | }); 470 | 471 | describe('request with prompt including none with other values', function() { 472 | var err, ext; 473 | 474 | before(function(done) { 475 | chai.oauth2orize.grant(extensions()) 476 | .req(function(req) { 477 | req.query = {}; 478 | req.query.prompt = 'none login'; 479 | }) 480 | .parse(function(e, o) { 481 | err = e; 482 | ext = o; 483 | done(); 484 | }) 485 | .authorize(); 486 | }); 487 | 488 | it('should throw error', function() { 489 | expect(err).to.be.an.instanceOf(Error); 490 | expect(err.constructor.name).to.equal('AuthorizationError'); 491 | expect(err.message).to.equal('Prompt includes none with other values'); 492 | }); 493 | }); 494 | 495 | }); 496 | 497 | }); 498 | --------------------------------------------------------------------------------