├── .gitignore ├── .travis.yml ├── LICENSE.MD ├── Makefile ├── README.MD ├── index.js ├── lib ├── jwt.js └── lodash.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | 3 | ### Node template 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 30 | node_modules 31 | 32 | # Atlassian 33 | atlassian-ide-plugin.xml 34 | 35 | # IDE 36 | .idea 37 | 38 | # Test keys 39 | test/*.pem 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "iojs" 7 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2014 Patrick Baker 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | keys: 2 | # RSASSA 3 | @openssl genrsa 2048 > test/rsa-private.pem 4 | @openssl genrsa 2048 > test/rsa-wrong-private.pem 5 | @openssl rsa -in test/rsa-private.pem -pubout > test/rsa-public.pem 6 | @openssl rsa -in test/rsa-wrong-private.pem -pubout > test/rsa-wrong-public.pem 7 | 8 | # ECDSA 9 | @openssl ecparam -out test/ec256-private.pem -name prime256v1 -genkey 10 | @openssl ecparam -out test/ec256-wrong-private.pem -name prime256v1 -genkey 11 | @openssl ecparam -out test/ec384-private.pem -name secp384r1 -genkey 12 | @openssl ecparam -out test/ec384-wrong-private.pem -name secp384r1 -genkey 13 | @openssl ecparam -out test/ec512-private.pem -name secp521r1 -genkey 14 | @openssl ecparam -out test/ec512-wrong-private.pem -name secp521r1 -genkey 15 | @openssl ec -in test/ec256-private.pem -pubout > test/ec256-public.pem 16 | @openssl ec -in test/ec256-wrong-private.pem -pubout > test/ec256-wrong-public.pem 17 | @openssl ec -in test/ec384-private.pem -pubout > test/ec384-public.pem 18 | @openssl ec -in test/ec384-wrong-private.pem -pubout > test/ec384-wrong-public.pem 19 | @openssl ec -in test/ec512-private.pem -pubout > test/ec512-public.pem 20 | @openssl ec -in test/ec512-wrong-private.pem -pubout > test/ec512-wrong-public.pem 21 | 22 | clean: 23 | @rm test/*.pem 24 | 25 | .PHONY: keys 26 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # jwt-async [![Build Status](https://secure.travis-ci.org/patbaker82/node-jwt-async.png)](http://travis-ci.org/patbaker82/node-jwt-async) 2 | 3 | An async implementation of [JSON Web Tokens (JWT)](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html). 4 | 5 | This implementation was developed against `draft-ietf-oauth-json-web-token-32`. Note that this library leverages core node modules to implement the JWT spec. The only dependency is lodash, which is used to implement some conveniences. 6 | 7 | ## Why? 8 | 9 | In my search I found that not all node JWT implementations were built with async first in mind. Additionally I wanted something that allowed for more flexability, but was still able to do the heavy lifting. 10 | 11 | It's interesting to note that Node's Crypto.create methods are all **synchronous**. However the writeable streams to these methods are **asynchronous**. That means, with this library, the data you want to sign will be **asynchronously** streamed to the relevant crypto methods. The crypto methods themselves do not perform any heavy crypto computation, therefore its quite fast. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | $ npm install jwt-async 17 | ``` 18 | 19 | ## Testing 20 | 21 | ```bash 22 | $ npm test 23 | ``` 24 | 25 | ## Example Usage 26 | 27 | ### Simple example 28 | 29 | ```javascript 30 | var JWT = require('jwt-async'); 31 | 32 | // Create jwt instance 33 | // Defaults to HS256 algorithm 34 | var jwt = new JWT(); 35 | jwt.setSecret('secret'); 36 | 37 | // Sign the jwt 38 | jwt.sign({someClaim: 'data'}, function (err, data) { 39 | if (err) console.log(err); 40 | 41 | // Print signed JWT 42 | // Paste it into http://www.jwt.io 43 | console.log(data); 44 | 45 | // Verify it 46 | jwt.verify(data, function (err, data) { 47 | if (err) console.log(err); 48 | 49 | // Print the verified JWT 50 | // This is an object 51 | console.log(data); 52 | }); 53 | }); 54 | ``` 55 | 56 | ### Example with options hash 57 | 58 | For sake of claity, the options hash is used to set the jwt instance up with some boilerplate. Meaning, when signing or verifying it will use the options defined. These can be changed via instance methods documented below. ie `jwt.setSecret`, `jwt.setPrivateKey`, `jwt.setValidations`, `jwt.setClaims`, etc. 59 | 60 | ```javascript 61 | var JWT = require('jwt-async'); 62 | 63 | var options = { 64 | // Default crypto per sign 65 | crypto: { 66 | algorithm: 'HS512', 67 | secret: 'secret' 68 | }, 69 | // Default claims per sign 70 | claims: { 71 | // Automatically set 'Issued At' if true (epoch), or set to a number 72 | iat: true, 73 | // Set 'Not Before' claim 74 | nbf: Math.floor(Date.now() / 1000) - 60, 75 | // Set 'Expiration' claim 76 | exp: Math.floor(Date.now() / 1000) + 60, 77 | // Set a custom claim 78 | custom: 'this is a custom claim' 79 | }, 80 | // Default validations per verify 81 | validations: { 82 | custom: function (claims, next) { 83 | // Do custom validation of claims 84 | // IE verify audience with database, etc 85 | if (claims.custom === 'this is a custom claim') { 86 | next(); 87 | } else { 88 | next(new Error('BOOM')); 89 | } 90 | }, 91 | exp: true, 92 | nbf: true 93 | } 94 | }; 95 | 96 | // Create jwt instance 97 | var jwt = new JWT(options); 98 | 99 | // More claims that will be merged with the default options 100 | // These will not persist since we're sending them directly to the sign method 101 | var moreClaims = { 102 | anotherField: 'this is another field' 103 | } 104 | 105 | // Sign the jwt 106 | jwt.sign(moreClaims, function (err, data) { 107 | if (err) console.log(err); 108 | 109 | // Print signed JWT 110 | // Paste it into http://www.jwt.io 111 | console.log(data); 112 | 113 | // Verify it 114 | jwt.verify(data, function (err, data) { 115 | if (err) console.log(err); 116 | 117 | // Print the verified JWT 118 | // This is an object 119 | console.log(data); 120 | }); 121 | }); 122 | ``` 123 | 124 | ### Example using bluebird promises 125 | 126 | If you don't like callbacks, then feel free to use promises. Here's how you can use this library with a promise library such as bluebird. 127 | 128 | ```javascript 129 | var JWT = require('jwt-async'); 130 | var BPromise = require('bluebird'); 131 | 132 | var jwt = BPromise.promisifyAll(new JWT()); 133 | jwt.setSecret('secret'); 134 | 135 | jwt.signAsync() 136 | .then(function (signed) { 137 | return jwt.verifyAsync(signed); 138 | }) 139 | .then(function (claims) { 140 | console.log(claims); 141 | }) 142 | .catch(JWT.JWTError, function (e) { 143 | console.log(e); 144 | }); 145 | ``` 146 | 147 | ## API 148 | 149 | ### constructor 150 | 151 | #### new JWT([options]) 152 | 153 | The JWT constructor can take an optional options hash defining some boiler plate for the instance returned. Once the instance is created, these options can be easily changed by executed the appropriate instance method. Without an options hash the JWT instance returned will default to HS256 (HMAC) JWT signing. 154 | 155 | `options` 156 | 157 | The only default set, without options, is the algorithm. Expect everything else either to be not set or set to false. Also, if its not obvious, claims are for the signing process and validations are for the verify process. 158 | 159 | * crypto 160 | * algorithm (string) - See below for acceptable algorithms 161 | * secret (string|buffer) - If using `HS*`, this is the secret used for the symmetric signing 162 | * privateKey (string|buffer) - If using `RS*`, `ES*` this is the private key used for signing JWT's 163 | * publicKey (string|buffer) - If using `RS*`, `ES*` this is the public key used for verifying JWT's 164 | * claims 165 | * iat (boolean|number) If true will set epoch, or can be passed a number if you want something more custom 166 | * exp (number) The time, typically epoch, for when the JWT should not be valid AFTER 167 | * nbf (number) The time, typically epoch, for when the JWT should not be valid BEFORE 168 | * validations 169 | * exp (boolean) If true, the JWT's exp will be validated against the current time in epoch. If the JWT is > than the current time a JWTExpiredError will be raised. 170 | * nbf (boolean) If true, the JWT's nbf will be validated against the current time in epoch. If the JWT is < than the current time a JWTInvalidBeforeTimeError will be raised. 171 | * custom (function (claims, callback)) If a function is set (see example), any custom validation of the claims can be done. The function must take two args, claims and a callback. If the callback is called with an argument it will be converted to a JWTValidationError. Or you can pass a custom Error(). 172 | 173 | ##### Example 174 | 175 | Without options 176 | ```javascript 177 | var JWT = require('jwt-async'); 178 | var jwt = new JWT; 179 | ``` 180 | 181 | With options; see above for full example 182 | ```javascript 183 | var JWT = require('jwt-async'); 184 | var jwt = new JWT(options); 185 | ``` 186 | 187 | ### instance methods 188 | 189 | #### jwt.sign(claims, callback) 190 | 191 | `claims` 192 | 193 | * (object) Claims to be used for the **current** signing process; they will not be retained and used for the next sign() invocation. Note, if claims are passed in they will be merged with the claims defined when the jwt instance was setup with options OR any claims that were set with `jwt.setClaims`. 194 | 195 | `callback` 196 | 197 | * (function (err, data)) - Standard callback with an error object and data. The data is the signed JWT. 198 | 199 | ##### Example 200 | 201 | ```javascript 202 | // Without a claims object 203 | jwt.sign(null, function (err, data) { 204 | // Log out signed JWT 205 | console.log(data); 206 | }); 207 | 208 | // With a claims object to be used for this signing 209 | jwt.sign({customClaim: 'this is a custom claim'}, function (err, data) { 210 | // Log out signed JWT 211 | console.log(data); 212 | }); 213 | 214 | // Enable iat 215 | jwt.sign({iat: true}, function (err, data) { 216 | // Log out signed JWT 217 | console.log(data); 218 | }); 219 | 220 | // Disable iat and add nbf 221 | jwt.sign({iat: false, nbf: 1419405977}, function (err, data) { 222 | // Log out signed JWT 223 | console.log(data); 224 | }); 225 | ``` 226 | 227 | #### jwt.verify(encodedJWT, callback) 228 | 229 | `encodedJWT` 230 | 231 | * (string|buffer) - Encoded JWT to be verified. Make sure the jwt instance object has the appropriate algorithm set and/or secret/public key. 232 | 233 | `callback` 234 | 235 | * (function (err, data)) - Standard callback with an error object and data. The data is an object of the decoded & verified JWT. 236 | 237 | ##### Example 238 | 239 | ```javascript 240 | jwt.verify(data, function (err, data) { 241 | // log Error object 242 | if (err) console.log(err); 243 | 244 | // log decoded and verified JWT object 245 | console.log(data); 246 | }); 247 | ``` 248 | 249 | #### jwt.setValidations(validations) 250 | 251 | Note that validations are only processed AFTER a jwt is successfully verified. IE it was verified against the secret or public key. 252 | 253 | `validations` 254 | 255 | * (object) - Object of validations to be set on the instance. 256 | 257 | * custom (function (claims, callback)) - Set a custom validation function 258 | * nbf (boolean) - Set true to validate nbf in JWT 259 | * exp (boolean) - Set true to validate exp in JWT 260 | 261 | ##### Example 262 | 263 | ```javascript 264 | var validations = { 265 | custom: function (claims, callback) { 266 | // Custom logic here, return an error to fail the validation 267 | if (claims.customClaim === 10) callback(new Error('This is bad!'); 268 | 269 | // Return successful 270 | callback(); 271 | }, 272 | nbf: true, 273 | exp: true 274 | }; 275 | 276 | jwt.setValidations(validations); 277 | ``` 278 | 279 | #### jwt.setPrivateKey(key) 280 | 281 | `key` 282 | 283 | * (string|buffer) - Set the private key on the jwt instance 284 | 285 | ##### Example 286 | 287 | ```javascript 288 | var privateKey = fs.readFileSync('ec256-private.pem'); 289 | jwt.setPrivateKey(privateKey); 290 | ``` 291 | 292 | #### jwt.setPublicKey() 293 | 294 | `key` 295 | 296 | * (string|buffer) - Set the public key on the jwt instance 297 | 298 | ##### Example 299 | 300 | ```javascript 301 | var publicKey = fs.readFileSync('ec256-public.pem'); 302 | jwt.setPrivateKey(publicKey); 303 | ``` 304 | 305 | #### jwt.setAlgorithm(algorithm) 306 | 307 | `algorithm` 308 | 309 | * (string) - Set algorithm on the jwt instance; see below for a table of supported algorithms. 310 | 311 | ##### Example 312 | 313 | ```javascript 314 | jwt.setAlgorithm('ES512'); 315 | ``` 316 | 317 | #### jwt.setClaims(claims) 318 | 319 | `claims` 320 | 321 | * (object) - Claims to be set on the jwt instance. These claims will be used for each signing invocation. If any claims are passed to sign(), they will be merged with the claims set on the instance. 322 | 323 | ##### Example 324 | 325 | ```javascript 326 | var claims = { 327 | // If iat is set to true, an epoch date will be generated automatically. 328 | iat: true, 329 | customClaim: 'this is a custom claim' 330 | }; 331 | 332 | jwt.setClaims(claims); 333 | ``` 334 | #### jwt.isHmac() 335 | 336 | * Returns true if algorithm selected is hmac based. 337 | 338 | #### jwt.isSign() 339 | 340 | * Returns true if algorithm selected uses keys to sign JWT 341 | 342 | #### jwt.isUnsecured() 343 | 344 | * Returns true if algorithm is set to 'NONE' 345 | 346 | #### jwt.getAlgorithm() 347 | 348 | * Returns the algorithm set on the instance 349 | 350 | #### jwt.getCrypto() 351 | 352 | * Returns the underlying node crypto algorithm that will be used 353 | 354 | #### jwt.getHeader() 355 | 356 | * Returns the set of headers set on the instance 357 | 358 | #### jwt.getClaims() 359 | 360 | * Returns the set of claims set of the instance 361 | 362 | #### jwt.getSecret() 363 | 364 | * Returns the secret set on the instance 365 | 366 | #### jwt.getPrivateKey() 367 | 368 | * Returns the privateKey set on the instance 369 | 370 | #### jwt.getPublicKey() 371 | 372 | * Returns the public key set on the instance 373 | 374 | #### jwt.getValidations() 375 | 376 | * Returns the validations set on the instance 377 | 378 | ### class methods 379 | 380 | #### JWT.getSupportedAlgorithms() 381 | 382 | * Returns (object) of the supported algorithms that this library supports 383 | 384 | #### JWT.base64urlDecode(str) 385 | #### JWT.base64urlUnescape(str) 386 | #### JWT.base64urlEncode(str) 387 | #### JWT.base64urlEscape(str) 388 | 389 | ### Errors 390 | 391 | For methods that are callback based an error will be set on callback if neccessary. 392 | 393 | #### JWTError 394 | 395 | JWTError is typically raised for generic type of error events. IE you tried to sign a JWT with algorithm ES256, but you don't have a privateKey set. 396 | 397 | #### JWTValidationError 398 | 399 | JWTValidationError is raised when a JWT fails the basic validation process. IE either the library wasn't able to verify that the JWT signed correctly, it was tampered with, unparseable, etc. 400 | 401 | #### JWTExpiredError 402 | 403 | JWTExpiredError is raised when the current time is after the JWT claims exp time. This error also exposes an `expiredAt` property on the error object that reflects when the JWT expired. Note that you must have exp validations enabled. 404 | 405 | #### JWTInvalidBeforeTimeError 406 | 407 | JWTInvalidBeforeTimeError is raised when the current time is before the JWT claims nbf time. This error also exposes an `invalidBefore` property on the error object that reflects when the JWT will be valid after. Note that you must have nbf validations enabled. 408 | 409 | ## Algorithms supported 410 | 411 | Algorithm | Description| Supported? 412 | ----------------|----------------|---------------- 413 | none | No digital signature or MAC performed | ✓ 414 | HS256 | HMAC using SHA-256 | ✓ 415 | HS384 | HMAC using SHA-384 | ✓ 416 | HS512 | HMAC using SHA-512 | ✓ 417 | RS256 | RSASSA-PKCS-v1_5 using SHA-256| ✓ 418 | RS384 | RSASSA-PKCS-v1_5 using SHA-384 | ✓ 419 | RS512 | RSASSA-PKCS-v1_5 using SHA-512 | ✓ 420 | ES256 | ECDSA using P-256 and SHA-256 | ✓ 421 | ES384 | ECDSA using P-384 and SHA-384 | ✓ 422 | ES512 | ECDSA using P-521 and SHA-512 | ✓ 423 | PS256 | RSASSA-PSS using SHA-256 and MGF1 with SHA-256 | ✖ 424 | PS384 | RSASSA-PSS using SHA-384 and MGF1 with SHA-384 | ✖ 425 | PS512 | RSASSA-PSS using SHA-512 and MGF1 with SHA-512 | ✖ 426 | 427 | # Caveats 428 | 429 | X.509 certificate support is not implemented 430 | 431 | # License 432 | 433 | MIT 434 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/jwt'); 2 | -------------------------------------------------------------------------------- /lib/jwt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jwt-async 3 | * 4 | * JSON Web Token (JWT) with asynchronicity 5 | * 6 | * Copyright(c) 2014 Patrick Baker 7 | * MIT Licensed 8 | */ 9 | 'use strict'; 10 | 11 | /** 12 | * node dependencies 13 | */ 14 | var Crypto = require('crypto'); 15 | var Util = require('util'); 16 | 17 | /** 18 | * external dependencies 19 | */ 20 | var _ = require('./lodash'); 21 | 22 | /** 23 | * Error messages 24 | */ 25 | var ERROR_CALLBACK_UNDEFINED = 'callback is undefined'; 26 | var ERROR_BAD_SIGNATURE = 'JWT signature does not match - possible tampering detected'; 27 | var ERROR_EXPIRED = 'JWT expired at'; 28 | var ERROR_NOT_VALID_UNTIL = 'JWT not valid until'; 29 | var ERROR_CLAIMS_UNDEFINED = 'claims is undefined'; 30 | var ERROR_UNKNOWN_SIGN_METHOD = 'unable to determine JWT signing type - asymmetrical or symmetrical'; 31 | var ERROR_PRIVATE_KEY_REQUIRED = 'private key is required for asymmetrical signing'; 32 | var ERROR_PUBLIC_KEY_REQUIRED = 'public key is required for asymmetrical signature verification'; 33 | var ERROR_SECRET_REQUIRED = 'secret required for symmetrical signing'; 34 | var ERROR_JWT_UNDEFINED = 'JWT is undefined'; 35 | var ERROR_JWT_FORMAT = 'JWT is not in the expected format'; 36 | var ERROR_UNKNOWN_VALIDATION = 'unable to validate claims - unknown error'; 37 | var ERROR_JWT_UNPARSABLE = 'JWT is unparsable'; 38 | var JWT_UNKNOWN_ALGORITHM = 'provided algorithm is unknown to the JWT spec'; 39 | 40 | /** 41 | * Create an instance of JWT 42 | * @constructor 43 | * @returns {Object} JWT instance 44 | * 45 | */ 46 | function JWT (options) { 47 | options = options || {}; 48 | var optionDefaults = { 49 | crypto: { 50 | algorithm: 'HS256' 51 | }, 52 | header: { 53 | typ: 'JWT' 54 | }, 55 | claims: {}, 56 | validations: {} 57 | }; 58 | _.defaultsDeep(options, optionDefaults); 59 | 60 | // -- Private 61 | var isHmac = false; 62 | var isSign = false; 63 | var isUnsecured = false; 64 | var validations; 65 | 66 | var algPrefix; 67 | var algBitLength; 68 | var algorithm; 69 | var crypto; 70 | 71 | var privateKey; 72 | var publicKey; 73 | var secret; 74 | 75 | var jwtClaims; 76 | var jwtHeader; 77 | 78 | function processKey (key) { 79 | if (!_.isUndefined(key)) { 80 | if (Buffer.isBuffer(key)) { 81 | return key.toString(); 82 | } else { 83 | return key; 84 | } 85 | } else { 86 | return key; 87 | } 88 | } 89 | 90 | // -- Protected 91 | this.isHmac = function () { 92 | return isHmac; 93 | }; 94 | 95 | this.isSign = function () { 96 | return isSign; 97 | }; 98 | 99 | this.isUnsecured = function () { 100 | return isUnsecured; 101 | }; 102 | 103 | this.getAlgorithm = function () { 104 | return algorithm; 105 | }; 106 | 107 | this.getCrypto = function () { 108 | return crypto; 109 | }; 110 | 111 | this.getHeader = function () { 112 | return jwtHeader; 113 | }; 114 | 115 | this.getClaims = function () { 116 | return jwtClaims; 117 | }; 118 | 119 | this.getSecret = function () { 120 | return secret; 121 | }; 122 | 123 | this.getPrivateKey = function () { 124 | return privateKey; 125 | }; 126 | 127 | this.getPublicKey = function () { 128 | return publicKey; 129 | }; 130 | 131 | this.getValidations = function () { 132 | return validations; 133 | }; 134 | 135 | // Public 136 | this.setValidations = function (obj) { 137 | validations = obj || {}; 138 | return this; 139 | }; 140 | 141 | this.setPrivateKey = function (key) { 142 | privateKey = processKey.bind(this)(key) || undefined; 143 | return this; 144 | }; 145 | 146 | this.setPublicKey = function (key) { 147 | publicKey = processKey.bind(this)(key) || undefined; 148 | return this; 149 | }; 150 | 151 | this.setSecret = function (sec) { 152 | if (!_.isUndefined(sec)) { 153 | if (Buffer.isBuffer(sec)) { 154 | secret = sec.toString(); 155 | } else { 156 | secret = sec; 157 | } 158 | } else { 159 | secret = sec; 160 | } 161 | return this; 162 | }; 163 | 164 | this.setAlgorithm = function (alg) { 165 | if (_.has(JWT.getSupportedAlgorithms(), alg)) { 166 | jwtHeader = _.merge(options.header, { alg: alg.toUpperCase() }); 167 | if (alg === 'NONE') { 168 | isHmac = false; 169 | isSign = false; 170 | isUnsecured = true; 171 | crypto = ''; 172 | } else { 173 | algorithm = alg.toUpperCase(); 174 | algPrefix = alg.substring(0,2); 175 | algBitLength = alg.substring(2,5); 176 | 177 | // Set type key 178 | if (algPrefix === 'RS' 179 | || algPrefix === 'ES' 180 | || algPrefix === 'PS' 181 | ) { 182 | isHmac = false; 183 | isSign = true; 184 | isUnsecured = false; 185 | crypto = 'RSA-SHA' + algBitLength; 186 | } else { 187 | isHmac = true; 188 | isSign = false; 189 | isUnsecured = false; 190 | crypto = 'sha' + algBitLength; 191 | } 192 | } 193 | } else { 194 | throw new JWTError(JWT_UNKNOWN_ALGORITHM + ' ' + alg); 195 | } 196 | return this; 197 | }; 198 | 199 | this.setClaims = function(claims) { 200 | jwtClaims = claims; 201 | return this; 202 | }; 203 | 204 | //-- Initialization 205 | // Set header / alg 206 | this.setAlgorithm(options.crypto.algorithm); 207 | 208 | // Set secret / keys 209 | if (!_.isUndefined(options.crypto.secret)) { 210 | this.setSecret(options.crypto.secret); 211 | } 212 | 213 | if (!_.isUndefined(options.crypto.privateKey)) { 214 | this.setPrivateKey(options.crypto.privateKey); 215 | } 216 | 217 | if (!_.isUndefined(options.crypto.publicKey)) { 218 | this.setPublicKey(options.crypto.publicKey); 219 | } 220 | 221 | // Set default claims 222 | this.setClaims(options.claims); 223 | 224 | // Set Validations 225 | this.setValidations(options.validations); 226 | 227 | // Return instance 228 | return this; 229 | } 230 | 231 | /** 232 | * Sign a JWT 233 | * 234 | */ 235 | JWT.prototype.sign = function (claims, callback) { 236 | claims = claims || {}; 237 | _.defaultsDeep(claims, this.getClaims()); 238 | var jwt = []; 239 | 240 | if (typeof claims === 'function') { 241 | callback = claims; 242 | claims = {}; 243 | } 244 | 245 | if (_.isUndefined(callback)) { 246 | throw new JWTError(ERROR_CALLBACK_UNDEFINED); 247 | } 248 | 249 | // Process time based options 250 | if (!_.isUndefined(claims.iat)) { 251 | if (claims.iat === true) { 252 | claims.iat = Math.floor(Date.now() / 1000); 253 | } else if (!_.isNumber(claims.iat) 254 | || claims.iat === false) { 255 | delete claims.iat; 256 | } 257 | } 258 | 259 | // Build JWT header & claims 260 | jwt.push(JWT.base64urlEncode(JSON.stringify(this.getHeader()))); 261 | jwt.push(JWT.base64urlEncode(JSON.stringify(claims))); 262 | 263 | // Process HMAC signature 264 | if (this.isUnsecured()) { 265 | return callback(null, jwt.join('.') + '.'); 266 | } else { 267 | this.encode(jwt.join('.'), function (err, data) { 268 | if (err) return callback(err); 269 | jwt.push(data); 270 | return callback(null, jwt.join('.')); 271 | }); 272 | } 273 | }; 274 | 275 | /** 276 | * Verify a JWT 277 | * 278 | */ 279 | JWT.prototype.verify = function (jwt, callback) { 280 | var jwtSplit; 281 | var jwtHeader; 282 | var jwtClaims; 283 | var jwtSignature; 284 | var jwtObject = {}; 285 | var _this = this; 286 | 287 | //-- Pre flight 288 | if (typeof jwt === 'function') { 289 | callback = jwt; 290 | jwt = undefined; 291 | } 292 | 293 | if (typeof callback === 'undefined') { 294 | throw new JWTError(ERROR_CALLBACK_UNDEFINED); 295 | } 296 | 297 | if (typeof jwt === 'undefined') { 298 | return callback(new JWTError(ERROR_JWT_UNDEFINED)); 299 | } 300 | 301 | if (Buffer.isBuffer(jwt)) { 302 | jwtSplit = jwt.toString().split('.'); 303 | } else { 304 | jwtSplit = jwt.split('.'); 305 | } 306 | 307 | if (jwtSplit.length !== 3) { 308 | return callback(new JWTValidationError(ERROR_JWT_FORMAT)); 309 | } 310 | 311 | jwtHeader = JWT.base64urlDecode(jwtSplit[0]); 312 | jwtClaims = JWT.base64urlDecode(jwtSplit[1]); 313 | jwtSignature = jwtSplit[2]; 314 | 315 | try { 316 | jwtObject.header = JSON.parse(jwtHeader); 317 | } catch (e) { 318 | return callback(new JWTValidationError(ERROR_JWT_UNPARSABLE)); 319 | } 320 | 321 | try { 322 | jwtObject.claims = JSON.parse(jwtClaims); 323 | } catch (e) { 324 | return callback(new JWTValidationError(ERROR_JWT_UNPARSABLE)); 325 | } 326 | 327 | //-- Verify authenticity 328 | if (this.isUnsecured()) { 329 | this.verifyClaims(jwtClaims, function (err, validated) { 330 | if (err) return callback(err); 331 | if (validated === true) { 332 | return callback(null, jwtObject); 333 | } else { 334 | return callback(new JWTValidationError(ERROR_UNKNOWN_VALIDATION)); 335 | } 336 | }); 337 | } else if (this.isHmac()) { 338 | this.encode(JWT.base64urlEncode(jwtHeader) + '.' + JWT.base64urlEncode(jwtClaims), function (err, data) { 339 | if (err) return callback(err); 340 | if (!(data === jwtSignature)) { 341 | return callback(new JWTValidationError(ERROR_BAD_SIGNATURE)); 342 | } else { 343 | // Process JWT Claims 344 | _this.verifyClaims(jwtClaims, function (err, validated) { 345 | if (err) return callback(err); 346 | if (validated === true) { 347 | return callback(null, jwtObject); 348 | } else { 349 | return callback(new JWTValidationError()); 350 | } 351 | }); 352 | } 353 | }); 354 | // out of scope! we already entered a callback 355 | } else if (this.isSign()) { 356 | // Check for public key 357 | if (_.isUndefined(this.getPublicKey())) { 358 | return callback(new JWTError(ERROR_PUBLIC_KEY_REQUIRED)); 359 | } 360 | 361 | // Since this is the only place where we will validate a public key against a signature 362 | // I guess it doesn't make sense to abstract it out into its own method 363 | var cryptoVerifyStream = Crypto.createVerify(this.getCrypto()); 364 | 365 | // Emitters 366 | try { 367 | cryptoVerifyStream 368 | .on('error', function (err) { 369 | return callback(err); 370 | }); 371 | 372 | cryptoVerifyStream.write(JWT.base64urlEncode(jwtHeader) + '.' + JWT.base64urlEncode(jwtClaims), function () { 373 | if (cryptoVerifyStream.verify(_this.getPublicKey(), JWT.base64urlUnescape(jwtSignature), 'base64') === false) { 374 | return callback(new JWTError(ERROR_BAD_SIGNATURE)); 375 | } else { 376 | // Process JWT Claims 377 | _this.verifyClaims(jwtClaims, function (err, validated) { 378 | if (err) return callback(err); 379 | if (validated === true) { 380 | return callback(null, jwtObject); 381 | } else { 382 | return callback(new JWTValidationError(ERROR_UNKNOWN_VALIDATION)); 383 | } 384 | }); 385 | // out of scope! we already entered a callback 386 | } 387 | }); 388 | // out of scope! we already entered a callback 389 | } catch (e) { 390 | return callback(e); 391 | } 392 | } 393 | }; 394 | 395 | /** 396 | * Encode a JWT 397 | * 398 | */ 399 | JWT.prototype.encode = function (str, callback) { 400 | if (_.isUndefined(callback)) { 401 | return callback(new JWTError(ERROR_CALLBACK_UNDEFINED)); 402 | } 403 | 404 | if (this.isHmac()) { 405 | // Check for secret 406 | if (_.isUndefined(this.getSecret())) { 407 | return callback(new JWTError(ERROR_SECRET_REQUIRED)); 408 | } 409 | 410 | // Setup HMAC Stream 411 | try { 412 | var cryptoHmacStream = Crypto.createHmac(this.getCrypto(), this.getSecret()); 413 | 414 | // Emitters 415 | cryptoHmacStream 416 | .on('error', function (err) { 417 | return callback(err); 418 | }); 419 | 420 | // Write the data 421 | cryptoHmacStream.write(str, 'utf8', function () { 422 | cryptoHmacStream.end(); 423 | return callback(null, JWT.base64urlEncode(cryptoHmacStream.read())); 424 | }); 425 | // out of scope! we already entered a callback 426 | } catch (e) { 427 | return callback(e); 428 | } 429 | } else if (this.isSign()) { 430 | var _this = this; 431 | 432 | // Check for privateKey 433 | if (_.isUndefined(_this.getPrivateKey())) { 434 | return callback(new JWTError(ERROR_PRIVATE_KEY_REQUIRED)); 435 | } 436 | 437 | try { 438 | // Setup Crypto Stream 439 | var cryptoSignStream = Crypto.createSign(this.getCrypto()); 440 | 441 | // Emitters 442 | cryptoSignStream 443 | .on('error', function (err) { 444 | return callback(err); 445 | }); 446 | 447 | // Write the data 448 | cryptoSignStream.write(str, 'utf8', function () { 449 | cryptoSignStream.end(); 450 | return callback(null, JWT.base64urlEncode(cryptoSignStream.sign(_this.getPrivateKey()))); 451 | }); 452 | // out of scope! we already entered a callback 453 | } catch (e) { 454 | return callback(e); 455 | } 456 | } else { 457 | return callback(new JWTError(ERROR_UNKNOWN_SIGN_METHOD)); 458 | } 459 | }; 460 | 461 | /** 462 | * Verify JWT claims 463 | * 464 | */ 465 | JWT.prototype.verifyClaims = function (claims, callback) { 466 | // Standard validation 467 | if (_.isUndefined(callback)) { 468 | return callback(new JWTError(ERROR_CALLBACK_UNDEFINED)); 469 | } 470 | 471 | if (_.isUndefined(claims)) { 472 | return callback(new JWTValidationError(ERROR_CLAIMS_UNDEFINED)); 473 | } 474 | 475 | // Convert to object 476 | claims = JSON.parse(claims); 477 | 478 | // Get Validations 479 | var validationSettings = this.getValidations(); 480 | 481 | //-- Validations 482 | if (!_.isUndefined(validationSettings)) { 483 | // nbf (Not Before) 484 | if (!_.isUndefined(claims.nbf) 485 | && !_.isUndefined(validationSettings.nbf) 486 | && validationSettings.nbf === true 487 | ) { 488 | if (Date.now() / 1000 < claims.nbf) { 489 | return callback(new JWTInvalidBeforeTimeError(ERROR_NOT_VALID_UNTIL + ' ' + claims.nbf, claims.nbf)); 490 | } 491 | } 492 | 493 | // exp (Expiration Time) 494 | if (!_.isUndefined(claims.exp) 495 | && !_.isUndefined(validationSettings.exp) 496 | && validationSettings.exp === true 497 | ) { 498 | if (Date.now() / 1000 > claims.exp) { 499 | return callback(new JWTExpiredError(ERROR_EXPIRED + ' ' + claims.exp, claims.exp)); 500 | } 501 | } 502 | 503 | // Custom validations 504 | if (!_.isUndefined(validationSettings.custom) 505 | && validationSettings.custom instanceof Function 506 | ) { 507 | validationSettings.custom(claims, function(err, success) { 508 | // Error handling for custom 509 | // Can take string or type of Error 510 | if (err) { 511 | if (typeof err === 'string') { 512 | return callback(new JWTValidationError(err)) 513 | } else if (err instanceof Error) { 514 | return callback(err); 515 | } 516 | } else { 517 | // No error and in callback, so return true 518 | callback(null, true) 519 | } 520 | }); 521 | } else { 522 | // Last predicate disabled so return good 523 | callback(null, true) 524 | } 525 | } else { 526 | // Validations disabled 527 | callback(null, true) 528 | } 529 | }; 530 | 531 | /** 532 | * Object of supported algorithms 533 | * 534 | */ 535 | JWT.getSupportedAlgorithms = function () { 536 | return { 537 | NONE: 'No digital signature or MAC performed', 538 | HS256: 'HMAC using SHA-256', 539 | HS384: 'HMAC using SHA-384', 540 | HS512: 'HMAC using SHA-512', 541 | RS256: 'RSASSA-PKCS-v1_5 using SHA-256', 542 | RS384: 'RSASSA-PKCS-v1_5 using SHA-384', 543 | RS512: 'RSASSA-PKCS-v1_5 using SHA-512', 544 | ES256: 'ECDSA using P-256 and SHA-256', 545 | ES384: 'ECDSA using P-384 and SHA-384', 546 | ES512: 'ECDSA using P-521 and SHA-512' 547 | /** 548 | * Node doesn't have support for the PSS signature format 549 | PS256: 'RSASSA-PSS using SHA-256 and MGF1 with SHA-256', 550 | PS384: 'RSASSA-PSS using SHA-384 and MGF1 with SHA-384', 551 | PS512: 'RSASSA-PSS using SHA-512 and MGF1 with SHA-512' 552 | */ 553 | } 554 | }; 555 | 556 | /** 557 | * Utility 558 | * 559 | */ 560 | JWT.base64urlDecode = function (str) { 561 | return new Buffer(JWT.base64urlUnescape(str), 'base64').toString(); 562 | }; 563 | 564 | JWT.base64urlUnescape = function (str) { 565 | str += new Array(5 - str.length % 4).join('='); 566 | return str.replace(/\-/g, '+').replace(/_/g, '/'); 567 | }; 568 | 569 | JWT.base64urlEncode = function (str) { 570 | return JWT.base64urlEscape(new Buffer(str).toString('base64')); 571 | }; 572 | 573 | JWT.base64urlEscape = function (str) { 574 | return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 575 | }; 576 | 577 | /** 578 | * Error class 579 | * 580 | */ 581 | function JWTError (message) { 582 | Error.call(this); 583 | Error.captureStackTrace(this, JWTError); 584 | 585 | this.name = 'JWTError'; 586 | this.message = message; 587 | } 588 | Util.inherits(JWTError, Error); 589 | 590 | function JWTValidationError (message) { 591 | Error.call(this); 592 | Error.captureStackTrace(this, JWTValidationError); 593 | 594 | this.name = 'JWTValidationError'; 595 | this.message = message; 596 | } 597 | Util.inherits(JWTValidationError, JWTError); 598 | 599 | function JWTExpiredError (message, expiredAt) { 600 | Error.call(this); 601 | Error.captureStackTrace(this, JWTExpiredError); 602 | 603 | this.name = 'JWTExpiredError'; 604 | this.message = message; 605 | this.expiredAt = expiredAt; 606 | } 607 | Util.inherits(JWTExpiredError, JWTValidationError); 608 | 609 | function JWTInvalidBeforeTimeError (message, invalidBefore) { 610 | Error.call(this); 611 | Error.captureStackTrace(this, JWTInvalidBeforeTimeError); 612 | 613 | this.name = 'JWTInvalidBeforeTimeError'; 614 | this.message = message; 615 | this.invalidBefore = invalidBefore; 616 | } 617 | Util.inherits(JWTInvalidBeforeTimeError, JWTValidationError); 618 | 619 | /** 620 | * Module exportables 621 | * 622 | */ 623 | module.exports = JWT; 624 | module.exports.JWTError = JWTError; 625 | module.exports.JWTValidationError = JWTValidationError; 626 | module.exports.JWTExpiredError = JWTExpiredError; 627 | module.exports.JWTInvalidBeforeTimeError = JWTInvalidBeforeTimeError; 628 | -------------------------------------------------------------------------------- /lib/lodash.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jwt-async 3 | * 4 | * JSON Web Token (JWT) with asynchronicity 5 | * 6 | * Copyright(c) 2014 Patrick Baker 7 | * MIT Licensed 8 | */ 9 | 10 | var _ = require('lodash'); 11 | 12 | /** 13 | * Deep merges an object 14 | * 15 | */ 16 | 17 | _.mixin({ 18 | defaultsDeep: function (a, b) { 19 | return _.partialRight(_.merge, function deep(value, other) { 20 | return _.merge(value, other, deep); 21 | })(a, b); 22 | } 23 | }); 24 | 25 | 26 | module.exports = _; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwt-async", 3 | "version": "1.1.2", 4 | "description": "JSON Web Token (JWT) with asynchronicity", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make keys;./node_modules/.bin/mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/patbaker82/node-jwt-async" 12 | }, 13 | "keywords": [ 14 | "jwt", 15 | "async", 16 | "claims", 17 | "validations", 18 | "encode", 19 | "decode", 20 | "verify" 21 | ], 22 | "author": "Patrick Baker ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/patbaker82/node-jwt-async/issues" 26 | }, 27 | "homepage": "https://github.com/patbaker82/node-jwt-async", 28 | "devDependencies": { 29 | "chai": "^1.10.0", 30 | "mocha": "^2.0.1", 31 | "sinon": "^1.12.2", 32 | "sinon-chai": "^2.6.0" 33 | }, 34 | "engine": "node >= 0.10.0", 35 | "dependencies": { 36 | "lodash": "^2.4.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jwt-async 3 | * 4 | * JSON Web Token (JWT) with asynchronicity 5 | * 6 | * Copyright(c) 2014 Patrick Baker 7 | * MIT Licensed 8 | 9 | */ 10 | 'use strict'; 11 | 12 | /** 13 | * Test frameworks 14 | * 15 | */ 16 | var chai = require('chai'); 17 | var expect = chai.expect; 18 | var sinon = require("sinon"); 19 | var sinonChai = require("sinon-chai"); 20 | chai.use(sinonChai); 21 | 22 | /** 23 | * Other dependencies 24 | * 25 | */ 26 | var JWT = require('../lib/jwt'); 27 | var fs = require('fs'); 28 | var _ = require('lodash'); 29 | 30 | var keyMap = { 31 | ES256: { 32 | privateKey: fs.readFileSync('./test/ec256-private.pem'), 33 | publicKey: fs.readFileSync('./test/ec256-public.pem'), 34 | invalidPublic: fs.readFileSync('./test/ec256-wrong-public.pem') 35 | }, 36 | ES384: { 37 | privateKey: fs.readFileSync('./test/ec384-private.pem'), 38 | publicKey: fs.readFileSync('./test/ec384-public.pem'), 39 | invalidPublic: fs.readFileSync('./test/ec384-wrong-public.pem') 40 | }, 41 | ES512: { 42 | privateKey: fs.readFileSync('./test/ec512-private.pem'), 43 | publicKey: fs.readFileSync('./test/ec512-public.pem'), 44 | invalidPublic: fs.readFileSync('./test/ec512-wrong-public.pem') 45 | }, 46 | RS: { 47 | privateKey: fs.readFileSync('./test/rsa-private.pem'), 48 | publicKey: fs.readFileSync('./test/rsa-public.pem'), 49 | invalidPublic: fs.readFileSync('./test/rsa-wrong-public.pem') 50 | } 51 | }; 52 | 53 | describe('JWT', function () { 54 | 55 | var jwt; 56 | beforeEach(function newJWT () { 57 | jwt = new JWT; 58 | }); 59 | 60 | describe('when creating a vanilla instance', function () { 61 | it('should default to HS256', function () { 62 | expect(jwt.getAlgorithm()).to.be.a('string').and.equal('HS256'); 63 | }); 64 | 65 | it('should have a header set', function () { 66 | expect(jwt.getHeader()).to.be.a('object').and.eql( 67 | { 68 | alg: 'HS256', 69 | typ: 'JWT' 70 | } 71 | ); 72 | }); 73 | 74 | it('should have no claims', function () { 75 | expect(jwt.getClaims()).to.be.a('object').and.eql({}); 76 | }); 77 | 78 | it('should have no validations enabled', function () { 79 | expect(jwt.getValidations()).to.be.a('object').and.eql({}); 80 | }); 81 | }); 82 | 83 | describe('when passing options to constructor', function () { 84 | 85 | var date; 86 | var options; 87 | 88 | beforeEach(function () { 89 | date = Math.floor(Date.now() / 1000); 90 | options = { 91 | crypto: { 92 | algorithm: 'HS512', 93 | secret: 'test secret', 94 | privateKey: 'test privateKey', 95 | publicKey: 'test publicKey' 96 | }, 97 | header: { 98 | typ: 'JWT', 99 | custom: 'header' 100 | }, 101 | claims: { 102 | iat: date, 103 | nbf: date, 104 | exp: date 105 | }, 106 | validations: { 107 | nbf: date, 108 | exp: date, 109 | custom: function () { 110 | } 111 | } 112 | }; 113 | 114 | jwt = new JWT(options); 115 | }); 116 | 117 | it('should set options', function () { 118 | expect(jwt.getAlgorithm()).to.be.a('string').and.equal(options.crypto.algorithm); 119 | expect(jwt.getSecret()).to.be.a('string').and.equal(options.crypto.secret); 120 | expect(jwt.getPrivateKey()).to.be.a('string').and.equal(options.crypto.privateKey); 121 | expect(jwt.getPublicKey()).to.be.a('string').and.equal(options.crypto.publicKey); 122 | expect(jwt.getHeader()).to.be.a('object').and.eql(options.header); 123 | expect(jwt.getClaims()).to.be.a('object').and.eql(options.claims); 124 | expect(jwt.getValidations()).to.be.a('object').and.eql(options.validations); 125 | }); 126 | 127 | }); 128 | 129 | describe('when changing algorithms', function () { 130 | it('should change the algorithm', function () { 131 | jwt.setAlgorithm('HS512'); 132 | expect(jwt.getAlgorithm()).to.be.a('string').and.equal('HS512'); 133 | }); 134 | 135 | it('should select correct node crypto hashing mechanism', function () { 136 | for (var k in JWT.getSupportedAlgorithms()) { 137 | var algPrefix = k.substring(0,2); 138 | var algBitLength = k.substring(2,5) 139 | jwt.setAlgorithm(k); 140 | 141 | if (algPrefix === 'RS' 142 | || algPrefix === 'ES' 143 | || algPrefix === 'PS' 144 | ) { 145 | expect(jwt.getCrypto()).to.be.a('string').and.equal('RSA-SHA' + algBitLength); 146 | } else if (algPrefix === 'HS') { 147 | expect(jwt.getCrypto()).to.be.a('string').and.equal('sha' + algBitLength); 148 | } else { 149 | expect(jwt.getCrypto()).to.be.a('string').and.empty(); 150 | } 151 | } 152 | }); 153 | 154 | it('should not allow an invalid algorithm', function () { 155 | expect(jwt.setAlgorithm.bind(null, 'XXXX')).to.throw(JWT.JWTError); 156 | }); 157 | }); 158 | 159 | _.forOwn(JWT.getSupportedAlgorithms(), function (k, v) { 160 | 161 | 162 | describe('when signing with ' + v, function () { 163 | 164 | 165 | it('should sign successfully', function (done) { 166 | setupAlgorithm(v, jwt); 167 | jwt.sign(null, function (err, data) { 168 | expect(err).to.be.null; 169 | done(); 170 | }); 171 | }); 172 | 173 | it('should be in the correct format', function (done) { 174 | setupAlgorithm(v, jwt); 175 | jwt.sign(null, function (err, data) { 176 | expect(data.split('.').length).to.equal(3); 177 | done(); 178 | }); 179 | }); 180 | 181 | if (v === 'NONE') { 182 | it('should be missing signature if algorithm is none', function (done) { 183 | setupAlgorithm(v, jwt); 184 | jwt.sign(null, function (err, data) { 185 | expect(data.split('.')[2]).to.be.empty(); 186 | done(); 187 | }); 188 | }); 189 | } 190 | 191 | it('should throw an error if a callback is not defined', function (done) { 192 | setupAlgorithm(v, jwt); 193 | expect(jwt.sign.bind(jwt)).to.throw(JWT.JWTError); 194 | done(); 195 | }); 196 | 197 | it('should have default claims (setup when object created)', function (done) { 198 | var mockClaims = { 199 | iat: 1234, 200 | custom: 'test' 201 | }; 202 | 203 | setupAlgorithm(v, jwt); 204 | jwt.setClaims(mockClaims); 205 | jwt.sign(null, function (err, data) { 206 | expect(JSON.parse(JWT.base64urlDecode(data.split('.')[1]))).to.be.a('object').and.eql(mockClaims); 207 | done(); 208 | }); 209 | }); 210 | 211 | it('should merge claims with default when passed to .sign()', function (done) { 212 | var mockClaims = { 213 | iat: 1234, 214 | custom: 'test' 215 | }; 216 | 217 | var newMockClaims = { 218 | iat: false 219 | }; 220 | 221 | var expectedResult = { 222 | custom: 'test' 223 | }; 224 | 225 | setupAlgorithm(v, jwt); 226 | jwt.setClaims(mockClaims); 227 | jwt.sign(newMockClaims, function (err, data) { 228 | expect(JSON.parse(JWT.base64urlDecode(data.split('.')[1]))).to.be.a('object').and.eql(expectedResult); 229 | done(); 230 | }); 231 | }); 232 | }); 233 | 234 | describe('when verifying with ' + v, function () { 235 | it('should verify with correct signature', function (done) { 236 | setupAlgorithm(v, jwt); 237 | var mockObj = { 238 | header: { 239 | typ: 'JWT', 240 | alg: v 241 | }, 242 | claims: {} 243 | }; 244 | 245 | jwt.sign(null, function (signErr, signData) { 246 | expect(signErr instanceof Error).to.be.false; 247 | jwt.verify(signData, function (verifyErr, verifyData) { 248 | expect(verifyErr instanceof Error).to.be.false; 249 | expect(verifyData).to.be.an('object').and.eql(mockObj); 250 | done(); 251 | }); 252 | }); 253 | }); 254 | 255 | it('should not verify with incorrect signature', function (done) { 256 | var algPrefix = v.substr(0, 2); 257 | var algBitLength = v.substring(2,5) 258 | setupAlgorithm(v, jwt); 259 | 260 | jwt.sign(null, function (signErr, signData) { 261 | expect(signErr instanceof Error).to.be.false; 262 | 263 | if (algPrefix === 'ES') { 264 | jwt.setPublicKey(keyMap['ES'+algBitLength].invalidPublic); 265 | } else if (algPrefix === 'RS') { 266 | jwt.setPublicKey(keyMap['RS'].invalidPublic); 267 | } else if (algPrefix === 'HS' ) { 268 | jwt.setSecret('bad secret'); 269 | } 270 | 271 | jwt.verify(signData, function (verifyErr, verifyData) { 272 | if (v === 'NONE') { 273 | expect(verifyErr instanceof Error).to.be.false; 274 | } else { 275 | expect(verifyErr instanceof Error).to.be.true; 276 | } 277 | done(); 278 | }); 279 | }); 280 | }); 281 | 282 | it('should not verify with unparsable crypto header', function (done) { 283 | setupAlgorithm(v, jwt); 284 | jwt.sign(null, function (signErr, signData) { 285 | var parts = signData.split('.'); 286 | parts[0] = parts[0].substr(3); 287 | signData = parts.join('.'); 288 | 289 | jwt.verify(signData, function (verifyErr, verifyData) { 290 | expect(verifyErr instanceof JWT.JWTValidationError).to.be.true; 291 | done(); 292 | }); 293 | }); 294 | }); 295 | 296 | it('should not verify with unparsable claims header', function (done) { 297 | setupAlgorithm(v, jwt); 298 | jwt.sign(null, function (signErr, signData) { 299 | var parts = signData.split('.'); 300 | parts[0] = parts[1].substr(3); 301 | signData = parts.join('.'); 302 | 303 | jwt.verify(signData, function (verifyErr, verifyData) { 304 | expect(verifyErr instanceof JWT.JWTValidationError).to.be.true; 305 | done(); 306 | }); 307 | }); 308 | }); 309 | 310 | it('should not verify with unparsable signature header', function (done) { 311 | setupAlgorithm(v, jwt); 312 | jwt.sign(null, function (signErr, signData) { 313 | var parts = signData.split('.'); 314 | parts[0] = parts[2].substr(3); 315 | signData = parts.join('.'); 316 | 317 | jwt.verify(signData, function (verifyErr, verifyData) { 318 | expect(verifyErr instanceof JWT.JWTValidationError).to.be.true; 319 | done(); 320 | }); 321 | }); 322 | }); 323 | }); 324 | 325 | describe('when validating claims with ' + v, function () { 326 | it('should process custom validation func() when enabled', function (done) { 327 | // Spy 328 | var spy = sinon.spy(function (claims, next) { 329 | next(); 330 | }); 331 | 332 | // Bootstrap instance 333 | setupAlgorithm(v, jwt); 334 | 335 | // Setup validations with custom hook 336 | jwt.setValidations({ 337 | custom: spy 338 | }); 339 | 340 | jwt.sign(null, function (signErr, signData) { 341 | jwt.verify(signData, function (verifyErr, verifyData) { 342 | expect(spy).to.be.spy; 343 | expect(spy).to.have.been.calledOnce; 344 | done(); 345 | }); 346 | }); 347 | }); 348 | 349 | it('should process custom validation func() err', function (done) { 350 | // Stub with fail 351 | var spy = sinon.spy(function (claims, next) { 352 | next(new JWT.JWTValidationError('test error')); 353 | }); 354 | 355 | // Bootstrap instance 356 | setupAlgorithm(v, jwt); 357 | 358 | // Setup validations with custom hook 359 | jwt.setValidations({ 360 | custom: spy 361 | }); 362 | 363 | jwt.sign(null, function (signErr, signData) { 364 | jwt.verify(signData, function (verifyErr, verifyData) { 365 | expect(spy).to.be.spy; 366 | expect(spy).to.have.been.calledOnce; 367 | expect(verifyErr instanceof JWT.JWTValidationError).to.be.true; 368 | done(); 369 | }); 370 | }); 371 | }); 372 | 373 | it('should validate nbf when enabled', function (done) { 374 | setupAlgorithm(v, jwt); 375 | 376 | // Put 60 seconds into future 377 | jwt.setClaims({ 378 | nbf: Math.floor(Date.now() / 1000) + 60 379 | }); 380 | 381 | jwt.setValidations({ 382 | nbf: true 383 | }); 384 | 385 | // Check it errors 386 | jwt.sign(null, function (signErr, signData) { 387 | jwt.verify(signData, function (verifyErr, verifyData) { 388 | expect(verifyErr instanceof JWT.JWTInvalidBeforeTimeError).to.be.true; 389 | expect(verifyErr.invalidBefore).to.be.a('number'); 390 | 391 | // Put 60 seconds into the past 392 | jwt.setClaims({ 393 | nbf: Math.floor(Date.now() / 1000) - 60 394 | }); 395 | 396 | jwt.sign(null, function (signErr, signData) { 397 | jwt.verify(signData, function (verifyErr, verifyData) { 398 | expect(verifyErr).to.be.null; 399 | expect(verifyData).to.be.a('object'); 400 | done(); 401 | }); 402 | }); 403 | }); 404 | }); 405 | }); 406 | 407 | it('should process exp when enabled', function (done) { 408 | setupAlgorithm(v, jwt); 409 | 410 | // Put 60 seconds into the past 411 | jwt.setClaims({ 412 | exp: Math.floor(Date.now() / 1000) - 60 413 | }); 414 | 415 | jwt.setValidations({ 416 | exp: true 417 | }); 418 | 419 | // Check it errors 420 | jwt.sign(null, function (signErr, signData) { 421 | jwt.verify(signData, function (verifyErr, verifyData) { 422 | expect(verifyErr instanceof JWT.JWTExpiredError).to.be.true; 423 | expect(verifyErr.expiredAt).to.be.a('number'); 424 | 425 | // Put 60 seconds into future 426 | jwt.setClaims({ 427 | exp: Math.floor(Date.now() / 1000) + 60 428 | }); 429 | 430 | jwt.sign(null, function (signErr, signData) { 431 | jwt.verify(signData, function (verifyErr, verifyData) { 432 | expect(verifyErr).to.be.null; 433 | expect(verifyData).to.be.a('object'); 434 | done(); 435 | }); 436 | }); 437 | }); 438 | }); 439 | }); 440 | 441 | it('should process exp, nbf, custom when ALL enabled', function (done) { 442 | setupAlgorithm(v, jwt); 443 | 444 | // Spy for custom function 445 | var spy = sinon.spy(function (claims, next) { 446 | next(); 447 | }); 448 | 449 | // Make all passing 450 | jwt.setClaims({ 451 | exp: Math.floor(Date.now() / 1000) + 60, 452 | nbf: Math.floor(Date.now() / 1000) - 60 453 | }); 454 | 455 | jwt.setValidations({ 456 | exp: true, 457 | nbf: true, 458 | custom: spy 459 | }); 460 | 461 | jwt.sign(null, function (signErr, signData) { 462 | jwt.verify(signData, function (verifyErr, verifyData) { 463 | expect(spy).to.be.spy; 464 | expect(spy).to.have.been.calledOnce; 465 | expect(verifyErr).to.be.null; 466 | expect(verifyData).to.be.a('object'); 467 | done(); 468 | }); 469 | }); 470 | }); 471 | }); 472 | }); 473 | }); 474 | 475 | function setupAlgorithm (k, obj) { 476 | var algPrefix = k.substring(0,2); 477 | var algBitLength = k.substring(2,5) 478 | 479 | obj.setAlgorithm(k); 480 | 481 | if (algPrefix === 'RS') { 482 | obj.setPrivateKey(keyMap.RS.privateKey); 483 | obj.setPublicKey(keyMap.RS.publicKey); 484 | } else if (algPrefix === 'ES') { 485 | obj.setPrivateKey(keyMap['ES'+algBitLength].privateKey); 486 | obj.setPublicKey(keyMap['ES'+algBitLength].publicKey); 487 | } else if (algPrefix === 'HS') { 488 | obj.setSecret('test123'); 489 | } 490 | }; 491 | --------------------------------------------------------------------------------