├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── lib └── oauth2orize-jwt-bearer │ ├── errors │ └── authorizationerror.js │ ├── exchange.js │ ├── index.js │ └── utils.js ├── package.json └── test ├── exchange-test.js └── index-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Node.js 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | README.md 2 | Makefile 3 | doc/ 4 | examples/ 5 | test/ 6 | 7 | # Mac OS X 8 | .DS_Store 9 | 10 | # Node.js 11 | .npmignore 12 | node_modules/ 13 | npm-debug.log 14 | 15 | # Git 16 | .git* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | - 0.8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2013 xTuple 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES = lib/**/*.js 2 | 3 | # ============================================================================== 4 | # Node Tests 5 | # ============================================================================== 6 | 7 | VOWS = ./node_modules/.bin/vows 8 | TESTS ?= test/*-test.js 9 | 10 | test: 11 | @NODE_ENV=test NODE_PATH=lib $(VOWS) $(TESTS) 12 | 13 | # ============================================================================== 14 | # Static Analysis 15 | # ============================================================================== 16 | 17 | JSHINT = jshint 18 | 19 | hint: lint 20 | lint: 21 | $(JSHINT) $(SOURCES) 22 | 23 | 24 | .PHONY: test hint lint -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | oauth2orize-jwt-bearer 2 | ====================== 3 | 4 | JSON Web Token (JWT) Bearer Token Exchange Middleware for [OAuth2orize](https://github.com/jaredhanson/oauth2orize). 5 | 6 | This module exchanges a JWT for an access token after authenticated, as [defined](http://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-01#section-2.1) by the JSON Web Token (JWT) Bearer Token Profiles for OAuth 2.0 draft. This module is modeled off of Google's OAuth 2.0 [Server to Server Applications](https://developers.google.com/accounts/docs/OAuth2ServiceAccount). This module can be used with the [passport-oauth2-jwt-bearer](https://github.com/xtuple/passport-oauth2-jwt-bearer) module to create a JWT OAuth 2.0 exchange scenario server. 7 | 8 | ## Install 9 | 10 | $ npm install oauth2orize-jwt-bearer 11 | 12 | ## Usage 13 | 14 | #### Register Exchange Middleware 15 | 16 | This exchange middleware is used to by clients to request an access token by using a JSON Web Token (JWT) generated by the client and verified by a Public Key stored on the OAuth 2.0 server. The exchange requires a verify callback, which accepts the client, JWT data and signature, then calls done providing a access token. 17 | 18 | ##### Key Generation Tips 19 | generate private key 20 | openssl genrsa -out private.pem 1024 21 | 22 | abstract public key 23 | openssl rsa -in private.pem -out public.pem -outform PEM -pubout 24 | 25 | sign the data 26 | signing data: echo -n "data-to-sign" | openssl dgst -RSA-SHA256 -sign private.pem > signed 27 | 28 | convert the signed file (binary) into base64 to be sent. 29 | base64 signed 30 | 31 | ```javascript 32 | var jwtBearer = require('oauth2orize-jwt-bearer').Exchange; 33 | 34 | server.exchange('urn:ietf:params:oauth:grant-type:jwt-bearer', jwtBearer(function(client, data, signature, done) { 35 | var crypto = require('crypto') 36 | , fs = require('fs') //load file system so you can grab the public key to read. 37 | , pub = fs.readFileSync('/path/to/public.pem').toString() //load PEM format public key as string, should be clients public key 38 | , verifier = crypto.createVerify("RSA-SHA256"); 39 | 40 | //verifier.update takes in a string of the data that is encrypted in the signature 41 | verifier.update(JSON.stringify(data)); 42 | 43 | if (verifier.verify(pub, signature, 'base64')) { 44 | //base64url decode data 45 | var b64string = data; 46 | var buf = new Buffer(b64string, 'base64').toString('ascii'); 47 | 48 | // TODO - verify client_id, scope and expiration are valid from the buf variable above 49 | 50 | AccessToken.create(client, scope, function(err, accessToken) { 51 | if (err) { return done(err); } 52 | done(null, accessToken); 53 | }); 54 | } 55 | })); 56 | ``` 57 | 58 | ## Tests 59 | 60 | $ npm install --dev 61 | $ make test 62 | 63 | ## Credits 64 | 65 | - [bendiy](http://github.com/bendiy) 66 | 67 | ## License 68 | 69 | [The MIT License](http://opensource.org/licenses/MIT) 70 | 71 | Copyright (c) 2012-2013 xTuple <[http://www.xtuple.com/](http://www.xtuple.com/)> 72 | -------------------------------------------------------------------------------- /lib/oauth2orize-jwt-bearer/errors/authorizationerror.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `AuthorizationError` error. 3 | * 4 | * @api public 5 | */ 6 | function AuthorizationError(message, code, uri, status) { 7 | if (!status) { 8 | switch (code) { 9 | case 'invalid_client': status = 401; break; 10 | case 'access_denied': status = 403; break; 11 | case 'server_error': status = 500; break; 12 | case 'temporarily_unavailable': status = 503; break; 13 | } 14 | } 15 | 16 | Error.call(this); 17 | Error.captureStackTrace(this, arguments.callee); 18 | this.name = 'AuthorizationError'; 19 | this.message = message || null; 20 | this.code = code || 'server_error'; 21 | this.uri = uri; 22 | this.status = status || 400; 23 | }; 24 | 25 | /** 26 | * Inherit from `Error`. 27 | */ 28 | AuthorizationError.prototype.__proto__ = Error.prototype; 29 | 30 | 31 | /** 32 | * Expose `AuthorizationError`. 33 | */ 34 | module.exports = AuthorizationError; 35 | -------------------------------------------------------------------------------- /lib/oauth2orize-jwt-bearer/exchange.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var utils = require('./utils') 5 | , AuthorizationError = require('./errors/authorizationerror'); 6 | 7 | 8 | /** 9 | * JWT access tokens request. 10 | * 11 | * This exchange middleware is used to by clients to request an access token by 12 | * using a JSON Web Token (JWT) generated by the client and verified by a 13 | * Public Key stored on the server. 14 | * 15 | * Callbacks: 16 | * 17 | * This middleware requires an `issue` callback, for which the function 18 | * signature is as follows: 19 | * 20 | * function(client, data, signature, done) { ... } 21 | * 22 | * `client` is the authenticated client instance attempting to obtain an access 23 | * token. `data` is the JWT encoded header and claim set concatenated with a 24 | * period '.', `signature` is the signature portion of the JWT. `done` is 25 | * called to issue an access token: 26 | * 27 | * done(err, accessToken, params) 28 | * 29 | * `accessToken` is the access token that will be sent to the client. Any 30 | * additional `params` will be included in the response. If an error occurs, 31 | * `done` should be invoked with `err` set in idomatic Node.js fashion. 32 | * 33 | * Options: 34 | * 35 | * userProperty property of `req` which contains the authenticated client (default: 'user') 36 | * 37 | * Examples: 38 | * 39 | * server.exchange('urn:ietf:params:oauth:grant-type:jwt-bearer', oauth2orize.exchange.jwtBearer(function(client, data, signature, done) { 40 | * var crypto = require('crypto') 41 | * , pub = pubKey // TODO - Load your pubKey registered to the client from the file system or database 42 | * , verifier = crypto.createVerify("RSA-SHA256"); 43 | * 44 | * verifier.update(JSON.stringify(data)); 45 | * 46 | * if (verifier.verify(pub, signature, 'base64')) { 47 | * 48 | * // TODO - base64url decode data then verify client_id, scope and expiration are valid 49 | * 50 | * AccessToken.create(client, scope, function(err, accessToken) { 51 | * if (err) { return done(err); } 52 | * done(null, accessToken); 53 | * }); 54 | * } 55 | * })); 56 | * 57 | * References: 58 | * - [JSON Web Token (JWT) Bearer Token Profiles for OAuth 2.0](http://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-01) 59 | * - [JSON Web Token (JWT)](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06) 60 | * - [Using OAuth 2.0 for Server to Server Applications](https://developers.google.com/accounts/docs/OAuth2ServiceAccount) 61 | * 62 | * @param {Object} options 63 | * @param {Function} issue 64 | * @api public 65 | */ 66 | module.exports = function jwtBearer(options, issue) { 67 | if (typeof options == 'function') { 68 | issue = options; 69 | options = null; 70 | } 71 | options = options || {}; 72 | 73 | if (!issue) throw new Error('OAuth 2.0 jwtBearer exchange middleware requires an issue function.'); 74 | 75 | var userProperty = options.userProperty || 'user'; 76 | 77 | return function jwt_bearer(req, res, next) { 78 | if (!req.body) { return next(new Error('Request body not parsed. Use bodyParser middleware.')); } 79 | 80 | // The 'user' property of `req` holds the authenticated user. In the case 81 | // of the token endpoint, the property will contain the OAuth 2.0 client. 82 | var client = req[userProperty] 83 | , contents = [] 84 | , jwtBearer = req.body['assertion'] 85 | , separator = '.'; 86 | 87 | if (!jwtBearer) { return next(new AuthorizationError('missing assertion parameter', 'invalid_request')); } 88 | 89 | contents = jwtBearer.split(separator); 90 | 91 | if (!Array.isArray(contents)) { contents = [ contents ]; } 92 | 93 | function issued(err, accessToken, params) { 94 | if (err) { return next(err); } 95 | if (!accessToken) { return next(new AuthorizationError('invalid JWT', 'invalid_grant')); } 96 | 97 | var tok = {}; 98 | tok['access_token'] = accessToken; 99 | if (params) { utils.merge(tok, params); } 100 | tok['token_type'] = tok['token_type'] || 'bearer'; 101 | 102 | var json = JSON.stringify(tok); 103 | res.setHeader('Content-Type', 'application/json'); 104 | res.setHeader('Cache-Control', 'no-store'); 105 | res.setHeader('Pragma', 'no-cache'); 106 | res.end(json); 107 | } 108 | 109 | var arity = issue.length; 110 | if (arity == 5) { 111 | // contents[0] = header, contents[1] = claimSet, contents[2] = signature 112 | issue(client, contents[0], contents[1], contents[2], issued); 113 | } else if (arity == 4) { 114 | // contents[0] = header, contents[1] = claimSet, contents[2] = signature 115 | var data = contents[0] + separator + contents[1]; 116 | issue(client, data, contents[2], issued); 117 | } else { // arity == 3 118 | issue(client, jwtBearer, issued); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/oauth2orize-jwt-bearer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Exchange = require('./exchange'); 5 | 6 | 7 | /** 8 | * Framework version. 9 | */ 10 | require('pkginfo')(module, 'version'); 11 | 12 | /** 13 | * Expose constructors. 14 | */ 15 | exports.Exchange = Exchange; -------------------------------------------------------------------------------- /lib/oauth2orize-jwt-bearer/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Merge object b with object a. 3 | * 4 | * var a = { foo: 'bar' } 5 | * , b = { bar: 'baz' }; 6 | * 7 | * utils.merge(a, b); 8 | * // => { foo: 'bar', bar: 'baz' } 9 | * 10 | * @param {Object} a 11 | * @param {Object} b 12 | * @return {Object} 13 | * @api private 14 | */ 15 | exports.merge = function(a, b){ 16 | if (a && b) { 17 | for (var key in b) { 18 | a[key] = b[key]; 19 | } 20 | } 21 | return a; 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2orize-jwt-bearer", 3 | "version": "0.2.0", 4 | "description": "JSON Web Token (JWT) Bearer Token Exchange Middleware for OAuth2orize.", 5 | "keywords": ["oauth2orize", "oauth", "oauth2", "authn", "authentication", "authz", "authorization", "api", "jwt", "json", "token", "bearer"], 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/xtuple/oauth2orize-jwt-bearer.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/xtuple/oauth2orize-jwt-bearer/issues" 12 | }, 13 | "author": { 14 | "name": "bendiy", 15 | "email": "ben@xtuple.com", 16 | "url": "http://www.xtuple.com/" 17 | }, 18 | "licenses": [ { 19 | "type": "MIT", 20 | "url": "http://www.opensource.org/licenses/MIT" 21 | } ], 22 | "main": "./lib/oauth2orize-jwt-bearer", 23 | "dependencies": { 24 | "pkginfo": "0.2.x", 25 | "oauth2orize": "~0.1.0" 26 | }, 27 | "devDependencies": { 28 | "vows": "0.6.x" 29 | }, 30 | "scripts": { 31 | "test": "NODE_PATH=lib node_modules/.bin/vows test/*-test.js" 32 | }, 33 | "engines": { "node": ">= 0.4.0" } 34 | } -------------------------------------------------------------------------------- /test/exchange-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var util = require('util'); 4 | var jwtBearerExchange = require('oauth2orize-jwt-bearer/exchange'); 5 | 6 | 7 | function MockRequest() { 8 | } 9 | 10 | function MockResponse() { 11 | this._headers = {}; 12 | this._data = ''; 13 | } 14 | 15 | MockResponse.prototype.setHeader = function(name, value) { 16 | this._headers[name] = value; 17 | }; 18 | 19 | MockResponse.prototype.end = function(data, encoding) { 20 | this._data += data; 21 | if (this.done) { this.done(); } 22 | }; 23 | 24 | vows.describe('jwtBearerExchange').addBatch({ 25 | 26 | 'middleware': { 27 | topic: function() { 28 | return jwtBearerExchange(function() {}); 29 | }, 30 | 31 | 'should return a function named jwt_bearer' : function(fn) { 32 | assert.isFunction(fn); 33 | assert.equal(fn.name, 'jwt_bearer'); 34 | } 35 | }, 36 | 37 | 'middleware that issues an access token': { 38 | topic: function() { 39 | return jwtBearerExchange(function(client, jwt, done) { 40 | if (client.id == 'c123' && jwt == 'header.claimSet.signature') { 41 | done(null, 's3cr1t'); 42 | } else { 43 | return done(new Error('something is wrong')); 44 | } 45 | }); 46 | }, 47 | 48 | 'when handling a request': { 49 | topic: function(jwtBearerExchange) { 50 | var self = this; 51 | var req = new MockRequest(); 52 | req.user = { id: 'c123', name: 'Example' }; 53 | req.body = { assertion: 'header.claimSet.signature' }; 54 | 55 | var res = new MockResponse(); 56 | res.done = function() { 57 | self.callback(null, req, res); 58 | }; 59 | 60 | function next(err) { 61 | self.callback(new Error('should not be called')); 62 | } 63 | process.nextTick(function () { 64 | jwtBearerExchange(req, res, next); 65 | }); 66 | }, 67 | 68 | 'should not call next' : function(err, req, res, e) { 69 | assert.isNull(err); 70 | }, 71 | 'should set headers' : function(err, req, res) { 72 | assert.equal(res._headers['Content-Type'], 'application/json'); 73 | assert.equal(res._headers['Cache-Control'], 'no-store'); 74 | assert.equal(res._headers['Pragma'], 'no-cache'); 75 | }, 76 | 'should send response' : function(err, req, res) { 77 | assert.equal(res._data, '{"access_token":"s3cr1t","token_type":"bearer"}'); 78 | } 79 | } 80 | }, 81 | 82 | 'middleware that issues an access token and params': { 83 | topic: function() { 84 | return jwtBearerExchange(function(client, jwt, done) { 85 | if (client.id == 'c123' && jwt == 'header.claimSet.signature') { 86 | done(null, 's3cr1t', { 'expires_in': 3600 }); 87 | } else { 88 | return done(new Error('something is wrong')); 89 | } 90 | }); 91 | }, 92 | 93 | 'when handling a request': { 94 | topic: function(jwtBearerExchange) { 95 | var self = this; 96 | var req = new MockRequest(); 97 | req.user = { id: 'c123', name: 'Example' }; 98 | req.body = { assertion: 'header.claimSet.signature' }; 99 | 100 | var res = new MockResponse(); 101 | res.done = function() { 102 | self.callback(null, req, res); 103 | }; 104 | 105 | function next(err) { 106 | self.callback(new Error('should not be called')); 107 | } 108 | process.nextTick(function () { 109 | jwtBearerExchange(req, res, next); 110 | }); 111 | }, 112 | 113 | 'should not call next' : function(err, req, res, e) { 114 | assert.isNull(err); 115 | }, 116 | 'should set headers' : function(err, req, res) { 117 | assert.equal(res._headers['Content-Type'], 'application/json'); 118 | assert.equal(res._headers['Cache-Control'], 'no-store'); 119 | assert.equal(res._headers['Pragma'], 'no-cache'); 120 | }, 121 | 'should send response' : function(err, req, res) { 122 | assert.equal(res._data, '{"access_token":"s3cr1t","expires_in":3600,"token_type":"bearer"}'); 123 | } 124 | } 125 | }, 126 | 127 | 'middleware that issues an access token and params with token_type': { 128 | topic: function() { 129 | return jwtBearerExchange(function(client, jwt, done) { 130 | if (client.id == 'c123' && jwt == 'header.claimSet.signature') { 131 | done(null, 's3cr1t', { 'token_type': 'foo', 'expires_in': 3600 }); 132 | } else { 133 | return done(new Error('something is wrong')); 134 | } 135 | }); 136 | }, 137 | 138 | 'when handling a request': { 139 | topic: function(jwtBearerExchange) { 140 | var self = this; 141 | var req = new MockRequest(); 142 | req.user = { id: 'c123', name: 'Example' }; 143 | req.body = { assertion: 'header.claimSet.signature' }; 144 | 145 | var res = new MockResponse(); 146 | res.done = function() { 147 | self.callback(null, req, res); 148 | }; 149 | 150 | function next(err) { 151 | self.callback(new Error('should not be called')); 152 | } 153 | process.nextTick(function () { 154 | jwtBearerExchange(req, res, next); 155 | }); 156 | }, 157 | 158 | 'should not call next' : function(err, req, res, e) { 159 | assert.isNull(err); 160 | }, 161 | 'should set headers' : function(err, req, res) { 162 | assert.equal(res._headers['Content-Type'], 'application/json'); 163 | assert.equal(res._headers['Cache-Control'], 'no-store'); 164 | assert.equal(res._headers['Pragma'], 'no-cache'); 165 | }, 166 | 'should send response' : function(err, req, res) { 167 | assert.equal(res._data, '{"access_token":"s3cr1t","token_type":"foo","expires_in":3600}'); 168 | } 169 | } 170 | }, 171 | 172 | 'middleware that issues an access token based on data and signature': { 173 | topic: function() { 174 | return jwtBearerExchange(function(client, data, signature, done) { 175 | if (client.id == 'c123' && data == 'header.claimSet' && 176 | signature == 'signature') { 177 | done(null, 's3cr1t'); 178 | } else { 179 | return done(new Error('something is wrong')); 180 | } 181 | }); 182 | }, 183 | 184 | 'when handling a request': { 185 | topic: function(jwtBearerExchange) { 186 | var self = this; 187 | var req = new MockRequest(); 188 | req.user = { id: 'c123', name: 'Example' }; 189 | req.body = { assertion: 'header.claimSet.signature' }; 190 | 191 | var res = new MockResponse(); 192 | res.done = function() { 193 | self.callback(null, req, res); 194 | }; 195 | 196 | function next(err) { 197 | self.callback(new Error('should not be called')); 198 | } 199 | process.nextTick(function () { 200 | jwtBearerExchange(req, res, next); 201 | }); 202 | }, 203 | 204 | 'should not call next' : function(err, req, res, e) { 205 | assert.isNull(err); 206 | }, 207 | 'should set headers' : function(err, req, res) { 208 | assert.equal(res._headers['Content-Type'], 'application/json'); 209 | assert.equal(res._headers['Cache-Control'], 'no-store'); 210 | assert.equal(res._headers['Pragma'], 'no-cache'); 211 | }, 212 | 'should send response' : function(err, req, res) { 213 | assert.equal(res._data, '{"access_token":"s3cr1t","token_type":"bearer"}'); 214 | } 215 | } 216 | }, 217 | 218 | 'middleware that issues an access token based on header, claimSet and signature': { 219 | topic: function() { 220 | return jwtBearerExchange(function(client, header, claimSet, signature, done) { 221 | if (client.id == 'c123' && header == 'header' && 222 | claimSet == 'claimSet' && signature == 'signature') { 223 | done(null, 's3cr1t'); 224 | } else { 225 | return done(new Error('something is wrong')); 226 | } 227 | }); 228 | }, 229 | 230 | 'when handling a request': { 231 | topic: function(jwtBearerExchange) { 232 | var self = this; 233 | var req = new MockRequest(); 234 | req.user = { id: 'c123', name: 'Example' }; 235 | req.body = { assertion: 'header.claimSet.signature' }; 236 | 237 | var res = new MockResponse(); 238 | res.done = function() { 239 | self.callback(null, req, res); 240 | }; 241 | 242 | function next(err) { 243 | self.callback(new Error('should not be called')); 244 | } 245 | process.nextTick(function () { 246 | jwtBearerExchange(req, res, next); 247 | }); 248 | }, 249 | 250 | 'should not call next' : function(err, req, res, e) { 251 | assert.isNull(err); 252 | }, 253 | 'should set headers' : function(err, req, res) { 254 | assert.equal(res._headers['Content-Type'], 'application/json'); 255 | assert.equal(res._headers['Cache-Control'], 'no-store'); 256 | assert.equal(res._headers['Pragma'], 'no-cache'); 257 | }, 258 | 'should send response' : function(err, req, res) { 259 | assert.equal(res._data, '{"access_token":"s3cr1t","token_type":"bearer"}'); 260 | } 261 | } 262 | }, 263 | 264 | 'middleware with userProperty option that issues an access token': { 265 | topic: function() { 266 | return jwtBearerExchange({ userProperty: 'otheruser' }, function(client, jwt, done) { 267 | if (client.id == 'c123' && jwt == 'header.claimSet.signature') { 268 | done(null, 's3cr1t'); 269 | } else { 270 | return done(new Error('something is wrong')); 271 | } 272 | }); 273 | }, 274 | 275 | 'when handling a request': { 276 | topic: function(jwtBearerExchange) { 277 | var self = this; 278 | var req = new MockRequest(); 279 | req.otheruser = { id: 'c123', name: 'Example' }; 280 | req.body = { assertion: 'header.claimSet.signature' }; 281 | 282 | var res = new MockResponse(); 283 | res.done = function() { 284 | self.callback(null, req, res); 285 | }; 286 | 287 | function next(err) { 288 | self.callback(new Error('should not be called')); 289 | } 290 | process.nextTick(function () { 291 | jwtBearerExchange(req, res, next); 292 | }); 293 | }, 294 | 295 | 'should not call next' : function(err, req, res, e) { 296 | assert.isNull(err); 297 | }, 298 | 'should set headers' : function(err, req, res) { 299 | assert.equal(res._headers['Content-Type'], 'application/json'); 300 | assert.equal(res._headers['Cache-Control'], 'no-store'); 301 | assert.equal(res._headers['Pragma'], 'no-cache'); 302 | }, 303 | 'should send response' : function(err, req, res) { 304 | assert.equal(res._data, '{"access_token":"s3cr1t","token_type":"bearer"}'); 305 | } 306 | } 307 | }, 308 | 309 | 'middleware that does not issue an access token': { 310 | topic: function() { 311 | return jwtBearerExchange(function(client, jwt, done) { 312 | return done(null, false); 313 | }); 314 | }, 315 | 316 | 'when handling a request': { 317 | topic: function(jwtBearerExchange) { 318 | var self = this; 319 | var req = new MockRequest(); 320 | req.user = { id: 'c123', name: 'Example' }; 321 | req.body = { assertion: 'header.claimSet.signature' }; 322 | 323 | var res = new MockResponse(); 324 | res.done = function() { 325 | self.callback(new Error('should not be called')); 326 | }; 327 | 328 | function next(err) { 329 | self.callback(null, req, res, err); 330 | } 331 | process.nextTick(function () { 332 | jwtBearerExchange(req, res, next); 333 | }); 334 | }, 335 | 336 | 'should not respond to request' : function(err, req, res) { 337 | assert.isNull(err); 338 | }, 339 | 'should next with error' : function(err, req, res, e) { 340 | assert.instanceOf(e, Error); 341 | assert.equal(e.constructor.name, 'AuthorizationError'); 342 | assert.equal(e.code, 'invalid_grant'); 343 | assert.equal(e.message, 'invalid JWT'); 344 | } 345 | } 346 | }, 347 | 348 | 'middleware that errors while issuing an access token': { 349 | topic: function() { 350 | return jwtBearerExchange(function(client, jwt, done) { 351 | return done(new Error('something went wrong')); 352 | }); 353 | }, 354 | 355 | 'when handling a request': { 356 | topic: function(jwtBearerExchange) { 357 | var self = this; 358 | var req = new MockRequest(); 359 | req.user = { id: 'c123', name: 'Example' }; 360 | req.body = { assertion: 'header.claimSet.signature' }; 361 | 362 | var res = new MockResponse(); 363 | res.done = function() { 364 | self.callback(new Error('should not be called')); 365 | }; 366 | 367 | function next(err) { 368 | self.callback(null, req, res, err); 369 | } 370 | process.nextTick(function () { 371 | jwtBearerExchange(req, res, next); 372 | }); 373 | }, 374 | 375 | 'should not respond to request' : function(err, req, res) { 376 | assert.isNull(err); 377 | }, 378 | 'should next with error' : function(err, req, res, e) { 379 | assert.instanceOf(e, Error); 380 | assert.equal(e.message, 'something went wrong'); 381 | } 382 | } 383 | }, 384 | 385 | 'middleware that handles a request lacking a JWT': { 386 | topic: function() { 387 | return jwtBearerExchange(function(client, jwt, done) { 388 | return done(new Error('something went wrong')); 389 | }); 390 | }, 391 | 392 | 'when handling a request': { 393 | topic: function(jwtBearerExchange) { 394 | var self = this; 395 | var req = new MockRequest(); 396 | req.user = { id: 'c123', name: 'Example' }; 397 | req.body = {}; 398 | 399 | var res = new MockResponse(); 400 | res.done = function() { 401 | self.callback(new Error('should not be called')); 402 | }; 403 | 404 | function next(err) { 405 | self.callback(null, req, res, err); 406 | } 407 | process.nextTick(function () { 408 | jwtBearerExchange(req, res, next); 409 | }); 410 | }, 411 | 412 | 'should not respond to request' : function(err, req, res) { 413 | assert.isNull(err); 414 | }, 415 | 'should next with error' : function(err, req, res, e) { 416 | assert.instanceOf(e, Error); 417 | assert.equal(e.constructor.name, 'AuthorizationError'); 418 | assert.equal(e.code, 'invalid_request'); 419 | assert.equal(e.message, 'missing assertion parameter'); 420 | } 421 | } 422 | }, 423 | 424 | 'middleware that handles a request in which the body was not parsed': { 425 | topic: function() { 426 | return jwtBearerExchange(function(client, jwt, done) { 427 | return done(new Error('something went wrong')); 428 | }); 429 | }, 430 | 431 | 'when handling a request': { 432 | topic: function(jwtBearerExchange) { 433 | var self = this; 434 | var req = new MockRequest(); 435 | req.user = { id: 'c123', name: 'Example' }; 436 | 437 | var res = new MockResponse(); 438 | res.done = function() { 439 | self.callback(new Error('should not be called')); 440 | }; 441 | 442 | function next(err) { 443 | self.callback(null, req, res, err); 444 | } 445 | process.nextTick(function () { 446 | jwtBearerExchange(req, res, next); 447 | }); 448 | }, 449 | 450 | 'should not respond to request' : function(err, req, res) { 451 | assert.isNull(err); 452 | }, 453 | 'should next with error' : function(err, req, res, e) { 454 | assert.instanceOf(e, Error); 455 | assert.equal(e.message, 'Request body not parsed. Use bodyParser middleware.'); 456 | } 457 | } 458 | }, 459 | 460 | 'middleware constructed without an issue function': { 461 | 'should throw an error': function () { 462 | assert.throws(function() { jwtBearerExchange(); }); 463 | } 464 | } 465 | 466 | }).export(module); 467 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var util = require('util'); 4 | var jwtBearer = require('oauth2orize-jwt-bearer'); 5 | 6 | 7 | vows.describe('oauth2orize-jwt-bearer').addBatch({ 8 | 9 | 'module': { 10 | 'should report a version': function (x) { 11 | assert.isString(jwtBearer.version); 12 | }, 13 | 14 | 'should export Exchange': function (x) { 15 | assert.isFunction(jwtBearer.Exchange); 16 | }, 17 | }, 18 | 19 | }).export(module); --------------------------------------------------------------------------------