├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── changelog ├── decode.js ├── index.js ├── lib ├── JsonWebTokenError.js ├── NotBeforeError.js ├── TokenExpiredError.js ├── asymmetricKeyDetailsSupported.js ├── psSupported.js ├── rsaPssKeyDetailsSupported.js ├── timespan.js └── validateAsymmetricKey.js ├── opslevel.yml ├── package.json ├── sign.js ├── test ├── .eslintrc.json ├── async_sign.tests.js ├── buffer.tests.js ├── claim-aud.test.js ├── claim-exp.test.js ├── claim-iat.test.js ├── claim-iss.test.js ├── claim-jti.test.js ├── claim-nbf.test.js ├── claim-private.tests.js ├── claim-sub.tests.js ├── decoding.tests.js ├── dsa-private.pem ├── dsa-public.pem ├── ecdsa-private.pem ├── ecdsa-public-invalid.pem ├── ecdsa-public-x509.pem ├── ecdsa-public.pem ├── encoding.tests.js ├── expires_format.tests.js ├── header-kid.test.js ├── invalid_exp.tests.js ├── invalid_pub.pem ├── issue_147.tests.js ├── issue_304.tests.js ├── issue_70.tests.js ├── jwt.asymmetric_signing.tests.js ├── jwt.hs.tests.js ├── jwt.malicious.tests.js ├── noTimestamp.tests.js ├── non_object_values.tests.js ├── option-complete.test.js ├── option-maxAge.test.js ├── option-nonce.test.js ├── prime256v1-private.pem ├── priv.pem ├── pub.pem ├── rsa-private.pem ├── rsa-pss-invalid-salt-length-private.pem ├── rsa-pss-private.pem ├── rsa-public-key.pem ├── rsa-public-key.tests.js ├── rsa-public.pem ├── schema.tests.js ├── secp384r1-private.pem ├── secp521r1-private.pem ├── set_headers.tests.js ├── test-utils.js ├── undefined_secretOrPublickey.tests.js ├── validateAsymmetricKey.tests.js ├── verify.tests.js └── wrong_alg.tests.js └── verify.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # Thanks to https://github.com/teppeis-sandbox/circleci2-multiple-node-versions 4 | 5 | commands: 6 | test-nodejs: 7 | steps: 8 | - run: 9 | name: Versions 10 | command: npm version 11 | - checkout 12 | - run: 13 | name: Install dependencies 14 | command: npm install 15 | - run: 16 | name: Test 17 | command: npm test 18 | 19 | jobs: 20 | node-v12: 21 | docker: 22 | - image: node:12 23 | steps: 24 | - test-nodejs 25 | node-v14: 26 | docker: 27 | - image: node:14 28 | steps: 29 | - test-nodejs 30 | node-v16: 31 | docker: 32 | - image: node:16 33 | steps: 34 | - test-nodejs 35 | node-v18: 36 | docker: 37 | - image: node:18 38 | steps: 39 | - test-nodejs 40 | 41 | workflows: 42 | node-multi-build: 43 | jobs: 44 | - node-v12 45 | - node-v14 46 | - node-v16 47 | - node-v18 48 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 6 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true 9 | }, 10 | "rules": { 11 | "comma-style": "error", 12 | "dot-notation": "error", 13 | "indent": ["error", 2], 14 | "no-control-regex": "error", 15 | "no-div-regex": "error", 16 | "no-eval": "error", 17 | "no-implied-eval": "error", 18 | "no-invalid-regexp": "error", 19 | "no-trailing-spaces": "error", 20 | "no-undef": "error", 21 | "no-unused-vars": "error" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request_target: {} 5 | push: 6 | branches: ["master", "main"] 7 | jobs: 8 | semgrep: 9 | name: Scan 10 | runs-on: ubuntu-latest 11 | container: 12 | image: returntocorp/semgrep 13 | if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: semgrep ci 17 | env: 18 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonwebtoken 2 | 3 | | **Build** | **Dependency** | 4 | |-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| 5 | | [![Build Status](https://secure.travis-ci.org/auth0/node-jsonwebtoken.svg?branch=master)](http://travis-ci.org/auth0/node-jsonwebtoken) | [![Dependency Status](https://david-dm.org/auth0/node-jsonwebtoken.svg)](https://david-dm.org/auth0/node-jsonwebtoken) | 6 | 7 | 8 | An implementation of [JSON Web Tokens](https://tools.ietf.org/html/rfc7519). 9 | 10 | This was developed against `draft-ietf-oauth-json-web-token-08`. It makes use of [node-jws](https://github.com/brianloveswords/node-jws) 11 | 12 | # Install 13 | 14 | ```bash 15 | $ npm install jsonwebtoken 16 | ``` 17 | 18 | # Migration notes 19 | 20 | * [From v8 to v9](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9) 21 | * [From v7 to v8](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v7-to-v8) 22 | 23 | # Usage 24 | 25 | ### jwt.sign(payload, secretOrPrivateKey, [options, callback]) 26 | 27 | (Asynchronous) If a callback is supplied, the callback is called with the `err` or the JWT. 28 | 29 | (Synchronous) Returns the JsonWebToken as string 30 | 31 | `payload` could be an object literal, buffer or string representing valid JSON. 32 | > **Please _note_ that** `exp` or any other claim is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity. 33 | 34 | > If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`. 35 | 36 | `secretOrPrivateKey` is a string (utf-8 encoded), buffer, object, or KeyObject containing either the secret for HMAC algorithms or the PEM 37 | encoded private key for RSA and ECDSA. In case of a private key with passphrase an object `{ key, passphrase }` can be used (based on [crypto documentation](https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format)), in this case be sure you pass the `algorithm` option. 38 | When signing with RSA algorithms the minimum modulus length is 2048 except when the allowInsecureKeySizes option is set to true. Private keys below this size will be rejected with an error. 39 | 40 | `options`: 41 | 42 | * `algorithm` (default: `HS256`) 43 | * `expiresIn`: expressed in seconds or a string describing a time span [vercel/ms](https://github.com/vercel/ms). 44 | > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). 45 | * `notBefore`: expressed in seconds or a string describing a time span [vercel/ms](https://github.com/vercel/ms). 46 | > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). 47 | * `audience` 48 | * `issuer` 49 | * `jwtid` 50 | * `subject` 51 | * `noTimestamp` 52 | * `header` 53 | * `keyid` 54 | * `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token. 55 | * `allowInsecureKeySizes`: if true allows private keys with a modulus below 2048 to be used for RSA 56 | * `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided. 57 | 58 | 59 | 60 | > There are no default values for `expiresIn`, `notBefore`, `audience`, `subject`, `issuer`. These claims can also be provided in the payload directly with `exp`, `nbf`, `aud`, `sub` and `iss` respectively, but you **_can't_** include in both places. 61 | 62 | Remember that `exp`, `nbf` and `iat` are **NumericDate**, see related [Token Expiration (exp claim)](#token-expiration-exp-claim) 63 | 64 | 65 | The header can be customized via the `options.header` object. 66 | 67 | Generated jwts will include an `iat` (issued at) claim by default unless `noTimestamp` is specified. If `iat` is inserted in the payload, it will be used instead of the real timestamp for calculating other things like `exp` given a timespan in `options.expiresIn`. 68 | 69 | Synchronous Sign with default (HMAC SHA256) 70 | 71 | ```js 72 | var jwt = require('jsonwebtoken'); 73 | var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); 74 | ``` 75 | 76 | Synchronous Sign with RSA SHA256 77 | ```js 78 | // sign with RSA SHA256 79 | var privateKey = fs.readFileSync('private.key'); 80 | var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }); 81 | ``` 82 | 83 | Sign asynchronously 84 | ```js 85 | jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function(err, token) { 86 | console.log(token); 87 | }); 88 | ``` 89 | 90 | Backdate a jwt 30 seconds 91 | ```js 92 | var older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'shhhhh'); 93 | ``` 94 | 95 | #### Token Expiration (exp claim) 96 | 97 | The standard for JWT defines an `exp` claim for expiration. The expiration is represented as a **NumericDate**: 98 | 99 | > A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. This is equivalent to the IEEE Std 1003.1, 2013 Edition [POSIX.1] definition "Seconds Since the Epoch", in which each day is accounted for by exactly 86400 seconds, other than that non-integer values can be represented. See RFC 3339 [RFC3339] for details regarding date/times in general and UTC in particular. 100 | 101 | This means that the `exp` field should contain the number of seconds since the epoch. 102 | 103 | Signing a token with 1 hour of expiration: 104 | 105 | ```javascript 106 | jwt.sign({ 107 | exp: Math.floor(Date.now() / 1000) + (60 * 60), 108 | data: 'foobar' 109 | }, 'secret'); 110 | ``` 111 | 112 | Another way to generate a token like this with this library is: 113 | 114 | ```javascript 115 | jwt.sign({ 116 | data: 'foobar' 117 | }, 'secret', { expiresIn: 60 * 60 }); 118 | 119 | //or even better: 120 | 121 | jwt.sign({ 122 | data: 'foobar' 123 | }, 'secret', { expiresIn: '1h' }); 124 | ``` 125 | 126 | ### jwt.verify(token, secretOrPublicKey, [options, callback]) 127 | 128 | (Asynchronous) If a callback is supplied, function acts asynchronously. The callback is called with the decoded payload if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will be called with the error. 129 | 130 | (Synchronous) If a callback is not supplied, function acts synchronously. Returns the payload decoded if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will throw the error. 131 | 132 | > __Warning:__ When the token comes from an untrusted source (e.g. user input or external requests), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected 133 | 134 | `token` is the JsonWebToken string 135 | 136 | `secretOrPublicKey` is a string (utf-8 encoded), buffer, or KeyObject containing either the secret for HMAC algorithms, or the PEM 137 | encoded public key for RSA and ECDSA. 138 | If `jwt.verify` is called asynchronous, `secretOrPublicKey` can be a function that should fetch the secret or public key. See below for a detailed example 139 | 140 | As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138), there are other libraries that expect base64 encoded secrets (random bytes encoded using base64), if that is your case you can pass `Buffer.from(secret, 'base64')`, by doing this the secret will be decoded using base64 and the token verification will use the original random bytes. 141 | 142 | `options` 143 | 144 | * `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`. 145 | > If not specified a defaults will be used based on the type of key provided 146 | > * secret - ['HS256', 'HS384', 'HS512'] 147 | > * rsa - ['RS256', 'RS384', 'RS512'] 148 | > * ec - ['ES256', 'ES384', 'ES512'] 149 | > * default - ['RS256', 'RS384', 'RS512'] 150 | * `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. 151 | > Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]` 152 | * `complete`: return an object with the decoded `{ payload, header, signature }` instead of only the usual content of the payload. 153 | * `issuer` (optional): string or array of strings of valid values for the `iss` field. 154 | * `jwtid` (optional): if you want to check JWT ID (`jti`), provide a string value here. 155 | * `ignoreExpiration`: if `true` do not validate the expiration of the token. 156 | * `ignoreNotBefore`... 157 | * `subject`: if you want to check subject (`sub`), provide a value here 158 | * `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers 159 | * `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [vercel/ms](https://github.com/vercel/ms). 160 | > Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). 161 | * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. 162 | * `nonce`: if you want to check `nonce` claim, provide a string value here. It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes)) 163 | * `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided. 164 | 165 | ```js 166 | // verify a token symmetric - synchronous 167 | var decoded = jwt.verify(token, 'shhhhh'); 168 | console.log(decoded.foo) // bar 169 | 170 | // verify a token symmetric 171 | jwt.verify(token, 'shhhhh', function(err, decoded) { 172 | console.log(decoded.foo) // bar 173 | }); 174 | 175 | // invalid token - synchronous 176 | try { 177 | var decoded = jwt.verify(token, 'wrong-secret'); 178 | } catch(err) { 179 | // err 180 | } 181 | 182 | // invalid token 183 | jwt.verify(token, 'wrong-secret', function(err, decoded) { 184 | // err 185 | // decoded undefined 186 | }); 187 | 188 | // verify a token asymmetric 189 | var cert = fs.readFileSync('public.pem'); // get public key 190 | jwt.verify(token, cert, function(err, decoded) { 191 | console.log(decoded.foo) // bar 192 | }); 193 | 194 | // verify audience 195 | var cert = fs.readFileSync('public.pem'); // get public key 196 | jwt.verify(token, cert, { audience: 'urn:foo' }, function(err, decoded) { 197 | // if audience mismatch, err == invalid audience 198 | }); 199 | 200 | // verify issuer 201 | var cert = fs.readFileSync('public.pem'); // get public key 202 | jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer' }, function(err, decoded) { 203 | // if issuer mismatch, err == invalid issuer 204 | }); 205 | 206 | // verify jwt id 207 | var cert = fs.readFileSync('public.pem'); // get public key 208 | jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid' }, function(err, decoded) { 209 | // if jwt id mismatch, err == invalid jwt id 210 | }); 211 | 212 | // verify subject 213 | var cert = fs.readFileSync('public.pem'); // get public key 214 | jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid', subject: 'subject' }, function(err, decoded) { 215 | // if subject mismatch, err == invalid subject 216 | }); 217 | 218 | // alg mismatch 219 | var cert = fs.readFileSync('public.pem'); // get public key 220 | jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) { 221 | // if token alg != RS256, err == invalid signature 222 | }); 223 | 224 | // Verify using getKey callback 225 | // Example uses https://github.com/auth0/node-jwks-rsa as a way to fetch the keys. 226 | var jwksClient = require('jwks-rsa'); 227 | var client = jwksClient({ 228 | jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json' 229 | }); 230 | function getKey(header, callback){ 231 | client.getSigningKey(header.kid, function(err, key) { 232 | var signingKey = key.publicKey || key.rsaPublicKey; 233 | callback(null, signingKey); 234 | }); 235 | } 236 | 237 | jwt.verify(token, getKey, options, function(err, decoded) { 238 | console.log(decoded.foo) // bar 239 | }); 240 | 241 | ``` 242 | 243 |
244 | Need to peek into a JWT without verifying it? (Click to expand) 245 | 246 | ### jwt.decode(token [, options]) 247 | 248 | (Synchronous) Returns the decoded payload without verifying if the signature is valid. 249 | 250 | > __Warning:__ This will __not__ verify whether the signature is valid. You should __not__ use this for untrusted messages. You most likely want to use `jwt.verify` instead. 251 | 252 | > __Warning:__ When the token comes from an untrusted source (e.g. user input or external request), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected 253 | 254 | 255 | `token` is the JsonWebToken string 256 | 257 | `options`: 258 | 259 | * `json`: force JSON.parse on the payload even if the header doesn't contain `"typ":"JWT"`. 260 | * `complete`: return an object with the decoded payload and header. 261 | 262 | Example 263 | 264 | ```js 265 | // get the decoded payload ignoring signature, no secretOrPrivateKey needed 266 | var decoded = jwt.decode(token); 267 | 268 | // get the decoded payload and header 269 | var decoded = jwt.decode(token, {complete: true}); 270 | console.log(decoded.header); 271 | console.log(decoded.payload) 272 | ``` 273 | 274 |
275 | 276 | ## Errors & Codes 277 | Possible thrown errors during verification. 278 | Error is the first argument of the verification callback. 279 | 280 | ### TokenExpiredError 281 | 282 | Thrown error if the token is expired. 283 | 284 | Error object: 285 | 286 | * name: 'TokenExpiredError' 287 | * message: 'jwt expired' 288 | * expiredAt: [ExpDate] 289 | 290 | ```js 291 | jwt.verify(token, 'shhhhh', function(err, decoded) { 292 | if (err) { 293 | /* 294 | err = { 295 | name: 'TokenExpiredError', 296 | message: 'jwt expired', 297 | expiredAt: 1408621000 298 | } 299 | */ 300 | } 301 | }); 302 | ``` 303 | 304 | ### JsonWebTokenError 305 | Error object: 306 | 307 | * name: 'JsonWebTokenError' 308 | * message: 309 | * 'invalid token' - the header or payload could not be parsed 310 | * 'jwt malformed' - the token does not have three components (delimited by a `.`) 311 | * 'jwt signature is required' 312 | * 'invalid signature' 313 | * 'jwt audience invalid. expected: [OPTIONS AUDIENCE]' 314 | * 'jwt issuer invalid. expected: [OPTIONS ISSUER]' 315 | * 'jwt id invalid. expected: [OPTIONS JWT ID]' 316 | * 'jwt subject invalid. expected: [OPTIONS SUBJECT]' 317 | 318 | ```js 319 | jwt.verify(token, 'shhhhh', function(err, decoded) { 320 | if (err) { 321 | /* 322 | err = { 323 | name: 'JsonWebTokenError', 324 | message: 'jwt malformed' 325 | } 326 | */ 327 | } 328 | }); 329 | ``` 330 | 331 | ### NotBeforeError 332 | Thrown if current time is before the nbf claim. 333 | 334 | Error object: 335 | 336 | * name: 'NotBeforeError' 337 | * message: 'jwt not active' 338 | * date: 2018-10-04T16:10:44.000Z 339 | 340 | ```js 341 | jwt.verify(token, 'shhhhh', function(err, decoded) { 342 | if (err) { 343 | /* 344 | err = { 345 | name: 'NotBeforeError', 346 | message: 'jwt not active', 347 | date: 2018-10-04T16:10:44.000Z 348 | } 349 | */ 350 | } 351 | }); 352 | ``` 353 | 354 | 355 | ## Algorithms supported 356 | 357 | Array of supported algorithms. The following algorithms are currently supported. 358 | 359 | | alg Parameter Value | Digital Signature or MAC Algorithm | 360 | |---------------------|------------------------------------------------------------------------| 361 | | HS256 | HMAC using SHA-256 hash algorithm | 362 | | HS384 | HMAC using SHA-384 hash algorithm | 363 | | HS512 | HMAC using SHA-512 hash algorithm | 364 | | RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm | 365 | | RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm | 366 | | RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm | 367 | | PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0) | 368 | | PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) | 369 | | PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) | 370 | | ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm | 371 | | ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm | 372 | | ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm | 373 | | none | No digital signature or MAC value included | 374 | 375 | ## Refreshing JWTs 376 | 377 | First of all, we recommend you to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system. 378 | 379 | We are not comfortable including this as part of the library, however, you can take a look at [this example](https://gist.github.com/ziluvatar/a3feb505c4c0ec37059054537b38fc48) to show how this could be accomplished. 380 | Apart from that example there are [an issue](https://github.com/auth0/node-jsonwebtoken/issues/122) and [a pull request](https://github.com/auth0/node-jsonwebtoken/pull/172) to get more knowledge about this topic. 381 | 382 | # TODO 383 | 384 | * X.509 certificate chain is not checked 385 | 386 | ## Issue Reporting 387 | 388 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 389 | 390 | ## Author 391 | 392 | [Auth0](https://auth0.com) 393 | 394 | ## License 395 | 396 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 397 | -------------------------------------------------------------------------------- /bin/changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var changelog = require('conventional-changelog'); 4 | var semver_regex = /\bv?(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?\b/ig; 5 | 6 | const commitPartial = ` - {{header}} 7 | 8 | {{~!-- commit hash --}} {{#if @root.linkReferences}}([{{hash}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/{{@root.commit}}/{{hash}})){{else}}{{hash~}}{{/if}} 9 | 10 | {{~!-- commit references --}}{{#if references}}, closes{{~#each references}} {{#if @root.linkReferences}}[{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if this.repository}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}{{else}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}{{/if}}/{{@root.issue}}/{{this.issue}}){{else}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}{{/if}}{{/each}}{{/if}} 11 | `; 12 | 13 | const headerPartial = `## {{version}}{{#if title}} "{{title}}"{{/if}}{{#if date}} - {{date}}{{/if}} 14 | `; 15 | 16 | changelog({ 17 | releaseCount: 19, 18 | // preset: 'jshint' 19 | }, null, null, null, { 20 | transform: function (commit) { 21 | if (commit.header && semver_regex.exec(commit.header)) { 22 | return null; 23 | } 24 | return commit; 25 | }, 26 | commitPartial: commitPartial, 27 | headerPartial: headerPartial 28 | }).pipe(process.stdout); 29 | -------------------------------------------------------------------------------- /decode.js: -------------------------------------------------------------------------------- 1 | var jws = require('jws'); 2 | 3 | module.exports = function (jwt, options) { 4 | options = options || {}; 5 | var decoded = jws.decode(jwt, options); 6 | if (!decoded) { return null; } 7 | var payload = decoded.payload; 8 | 9 | //try parse the payload 10 | if(typeof payload === 'string') { 11 | try { 12 | var obj = JSON.parse(payload); 13 | if(obj !== null && typeof obj === 'object') { 14 | payload = obj; 15 | } 16 | } catch (e) { } 17 | } 18 | 19 | //return header if `complete` option is enabled. header includes claims 20 | //such as `kid` and `alg` used to select the key within a JWKS needed to 21 | //verify the signature 22 | if (options.complete === true) { 23 | return { 24 | header: decoded.header, 25 | payload: payload, 26 | signature: decoded.signature 27 | }; 28 | } 29 | return payload; 30 | }; 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | decode: require('./decode'), 3 | verify: require('./verify'), 4 | sign: require('./sign'), 5 | JsonWebTokenError: require('./lib/JsonWebTokenError'), 6 | NotBeforeError: require('./lib/NotBeforeError'), 7 | TokenExpiredError: require('./lib/TokenExpiredError'), 8 | }; 9 | -------------------------------------------------------------------------------- /lib/JsonWebTokenError.js: -------------------------------------------------------------------------------- 1 | var JsonWebTokenError = function (message, error) { 2 | Error.call(this, message); 3 | if(Error.captureStackTrace) { 4 | Error.captureStackTrace(this, this.constructor); 5 | } 6 | this.name = 'JsonWebTokenError'; 7 | this.message = message; 8 | if (error) this.inner = error; 9 | }; 10 | 11 | JsonWebTokenError.prototype = Object.create(Error.prototype); 12 | JsonWebTokenError.prototype.constructor = JsonWebTokenError; 13 | 14 | module.exports = JsonWebTokenError; 15 | -------------------------------------------------------------------------------- /lib/NotBeforeError.js: -------------------------------------------------------------------------------- 1 | var JsonWebTokenError = require('./JsonWebTokenError'); 2 | 3 | var NotBeforeError = function (message, date) { 4 | JsonWebTokenError.call(this, message); 5 | this.name = 'NotBeforeError'; 6 | this.date = date; 7 | }; 8 | 9 | NotBeforeError.prototype = Object.create(JsonWebTokenError.prototype); 10 | 11 | NotBeforeError.prototype.constructor = NotBeforeError; 12 | 13 | module.exports = NotBeforeError; -------------------------------------------------------------------------------- /lib/TokenExpiredError.js: -------------------------------------------------------------------------------- 1 | var JsonWebTokenError = require('./JsonWebTokenError'); 2 | 3 | var TokenExpiredError = function (message, expiredAt) { 4 | JsonWebTokenError.call(this, message); 5 | this.name = 'TokenExpiredError'; 6 | this.expiredAt = expiredAt; 7 | }; 8 | 9 | TokenExpiredError.prototype = Object.create(JsonWebTokenError.prototype); 10 | 11 | TokenExpiredError.prototype.constructor = TokenExpiredError; 12 | 13 | module.exports = TokenExpiredError; -------------------------------------------------------------------------------- /lib/asymmetricKeyDetailsSupported.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | module.exports = semver.satisfies(process.version, '>=15.7.0'); 4 | -------------------------------------------------------------------------------- /lib/psSupported.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver'); 2 | 3 | module.exports = semver.satisfies(process.version, '^6.12.0 || >=8.0.0'); 4 | -------------------------------------------------------------------------------- /lib/rsaPssKeyDetailsSupported.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | module.exports = semver.satisfies(process.version, '>=16.9.0'); 4 | -------------------------------------------------------------------------------- /lib/timespan.js: -------------------------------------------------------------------------------- 1 | var ms = require('ms'); 2 | 3 | module.exports = function (time, iat) { 4 | var timestamp = iat || Math.floor(Date.now() / 1000); 5 | 6 | if (typeof time === 'string') { 7 | var milliseconds = ms(time); 8 | if (typeof milliseconds === 'undefined') { 9 | return; 10 | } 11 | return Math.floor(timestamp + milliseconds / 1000); 12 | } else if (typeof time === 'number') { 13 | return timestamp + time; 14 | } else { 15 | return; 16 | } 17 | 18 | }; -------------------------------------------------------------------------------- /lib/validateAsymmetricKey.js: -------------------------------------------------------------------------------- 1 | const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('./asymmetricKeyDetailsSupported'); 2 | const RSA_PSS_KEY_DETAILS_SUPPORTED = require('./rsaPssKeyDetailsSupported'); 3 | 4 | const allowedAlgorithmsForKeys = { 5 | 'ec': ['ES256', 'ES384', 'ES512'], 6 | 'rsa': ['RS256', 'PS256', 'RS384', 'PS384', 'RS512', 'PS512'], 7 | 'rsa-pss': ['PS256', 'PS384', 'PS512'] 8 | }; 9 | 10 | const allowedCurves = { 11 | ES256: 'prime256v1', 12 | ES384: 'secp384r1', 13 | ES512: 'secp521r1', 14 | }; 15 | 16 | module.exports = function(algorithm, key) { 17 | if (!algorithm || !key) return; 18 | 19 | const keyType = key.asymmetricKeyType; 20 | if (!keyType) return; 21 | 22 | const allowedAlgorithms = allowedAlgorithmsForKeys[keyType]; 23 | 24 | if (!allowedAlgorithms) { 25 | throw new Error(`Unknown key type "${keyType}".`); 26 | } 27 | 28 | if (!allowedAlgorithms.includes(algorithm)) { 29 | throw new Error(`"alg" parameter for "${keyType}" key type must be one of: ${allowedAlgorithms.join(', ')}.`) 30 | } 31 | 32 | /* 33 | * Ignore the next block from test coverage because it gets executed 34 | * conditionally depending on the Node version. Not ignoring it would 35 | * prevent us from reaching the target % of coverage for versions of 36 | * Node under 15.7.0. 37 | */ 38 | /* istanbul ignore next */ 39 | if (ASYMMETRIC_KEY_DETAILS_SUPPORTED) { 40 | switch (keyType) { 41 | case 'ec': 42 | const keyCurve = key.asymmetricKeyDetails.namedCurve; 43 | const allowedCurve = allowedCurves[algorithm]; 44 | 45 | if (keyCurve !== allowedCurve) { 46 | throw new Error(`"alg" parameter "${algorithm}" requires curve "${allowedCurve}".`); 47 | } 48 | break; 49 | 50 | case 'rsa-pss': 51 | if (RSA_PSS_KEY_DETAILS_SUPPORTED) { 52 | const length = parseInt(algorithm.slice(-3), 10); 53 | const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails; 54 | 55 | if (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) { 56 | throw new Error(`Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${algorithm}.`); 57 | } 58 | 59 | if (saltLength !== undefined && saltLength > length >> 3) { 60 | throw new Error(`Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" ${algorithm}.`) 61 | } 62 | } 63 | break; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: iam_protocols 5 | tier: 6 | tags: 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonwebtoken", 3 | "version": "9.0.2", 4 | "description": "JSON Web Token implementation (symmetric and asymmetric)", 5 | "main": "index.js", 6 | "nyc": { 7 | "check-coverage": true, 8 | "lines": 95, 9 | "statements": 95, 10 | "functions": 100, 11 | "branches": 95, 12 | "exclude": [ 13 | "./test/**" 14 | ], 15 | "reporter": [ 16 | "json", 17 | "lcov", 18 | "text-summary" 19 | ] 20 | }, 21 | "scripts": { 22 | "lint": "eslint .", 23 | "coverage": "nyc mocha --use_strict", 24 | "test": "npm run lint && npm run coverage && cost-of-modules" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/auth0/node-jsonwebtoken" 29 | }, 30 | "keywords": [ 31 | "jwt" 32 | ], 33 | "author": "auth0", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/auth0/node-jsonwebtoken/issues" 37 | }, 38 | "dependencies": { 39 | "jws": "^3.2.2", 40 | "lodash.includes": "^4.3.0", 41 | "lodash.isboolean": "^3.0.3", 42 | "lodash.isinteger": "^4.0.4", 43 | "lodash.isnumber": "^3.0.3", 44 | "lodash.isplainobject": "^4.0.6", 45 | "lodash.isstring": "^4.0.1", 46 | "lodash.once": "^4.0.0", 47 | "ms": "^2.1.1", 48 | "semver": "^7.5.4" 49 | }, 50 | "devDependencies": { 51 | "atob": "^2.1.2", 52 | "chai": "^4.1.2", 53 | "conventional-changelog": "~1.1.0", 54 | "cost-of-modules": "^1.0.1", 55 | "eslint": "^4.19.1", 56 | "mocha": "^5.2.0", 57 | "nsp": "^2.6.2", 58 | "nyc": "^11.9.0", 59 | "sinon": "^6.0.0" 60 | }, 61 | "engines": { 62 | "npm": ">=6", 63 | "node": ">=12" 64 | }, 65 | "files": [ 66 | "lib", 67 | "decode.js", 68 | "sign.js", 69 | "verify.js" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /sign.js: -------------------------------------------------------------------------------- 1 | const timespan = require('./lib/timespan'); 2 | const PS_SUPPORTED = require('./lib/psSupported'); 3 | const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); 4 | const jws = require('jws'); 5 | const includes = require('lodash.includes'); 6 | const isBoolean = require('lodash.isboolean'); 7 | const isInteger = require('lodash.isinteger'); 8 | const isNumber = require('lodash.isnumber'); 9 | const isPlainObject = require('lodash.isplainobject'); 10 | const isString = require('lodash.isstring'); 11 | const once = require('lodash.once'); 12 | const { KeyObject, createSecretKey, createPrivateKey } = require('crypto') 13 | 14 | const SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']; 15 | if (PS_SUPPORTED) { 16 | SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); 17 | } 18 | 19 | const sign_options_schema = { 20 | expiresIn: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' }, 21 | notBefore: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' }, 22 | audience: { isValid: function(value) { return isString(value) || Array.isArray(value); }, message: '"audience" must be a string or array' }, 23 | algorithm: { isValid: includes.bind(null, SUPPORTED_ALGS), message: '"algorithm" must be a valid string enum value' }, 24 | header: { isValid: isPlainObject, message: '"header" must be an object' }, 25 | encoding: { isValid: isString, message: '"encoding" must be a string' }, 26 | issuer: { isValid: isString, message: '"issuer" must be a string' }, 27 | subject: { isValid: isString, message: '"subject" must be a string' }, 28 | jwtid: { isValid: isString, message: '"jwtid" must be a string' }, 29 | noTimestamp: { isValid: isBoolean, message: '"noTimestamp" must be a boolean' }, 30 | keyid: { isValid: isString, message: '"keyid" must be a string' }, 31 | mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' }, 32 | allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'}, 33 | allowInvalidAsymmetricKeyTypes: { isValid: isBoolean, message: '"allowInvalidAsymmetricKeyTypes" must be a boolean'} 34 | }; 35 | 36 | const registered_claims_schema = { 37 | iat: { isValid: isNumber, message: '"iat" should be a number of seconds' }, 38 | exp: { isValid: isNumber, message: '"exp" should be a number of seconds' }, 39 | nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' } 40 | }; 41 | 42 | function validate(schema, allowUnknown, object, parameterName) { 43 | if (!isPlainObject(object)) { 44 | throw new Error('Expected "' + parameterName + '" to be a plain object.'); 45 | } 46 | Object.keys(object) 47 | .forEach(function(key) { 48 | const validator = schema[key]; 49 | if (!validator) { 50 | if (!allowUnknown) { 51 | throw new Error('"' + key + '" is not allowed in "' + parameterName + '"'); 52 | } 53 | return; 54 | } 55 | if (!validator.isValid(object[key])) { 56 | throw new Error(validator.message); 57 | } 58 | }); 59 | } 60 | 61 | function validateOptions(options) { 62 | return validate(sign_options_schema, false, options, 'options'); 63 | } 64 | 65 | function validatePayload(payload) { 66 | return validate(registered_claims_schema, true, payload, 'payload'); 67 | } 68 | 69 | const options_to_payload = { 70 | 'audience': 'aud', 71 | 'issuer': 'iss', 72 | 'subject': 'sub', 73 | 'jwtid': 'jti' 74 | }; 75 | 76 | const options_for_objects = [ 77 | 'expiresIn', 78 | 'notBefore', 79 | 'noTimestamp', 80 | 'audience', 81 | 'issuer', 82 | 'subject', 83 | 'jwtid', 84 | ]; 85 | 86 | module.exports = function (payload, secretOrPrivateKey, options, callback) { 87 | if (typeof options === 'function') { 88 | callback = options; 89 | options = {}; 90 | } else { 91 | options = options || {}; 92 | } 93 | 94 | const isObjectPayload = typeof payload === 'object' && 95 | !Buffer.isBuffer(payload); 96 | 97 | const header = Object.assign({ 98 | alg: options.algorithm || 'HS256', 99 | typ: isObjectPayload ? 'JWT' : undefined, 100 | kid: options.keyid 101 | }, options.header); 102 | 103 | function failure(err) { 104 | if (callback) { 105 | return callback(err); 106 | } 107 | throw err; 108 | } 109 | 110 | if (!secretOrPrivateKey && options.algorithm !== 'none') { 111 | return failure(new Error('secretOrPrivateKey must have a value')); 112 | } 113 | 114 | if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) { 115 | try { 116 | secretOrPrivateKey = createPrivateKey(secretOrPrivateKey) 117 | } catch (_) { 118 | try { 119 | secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === 'string' ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey) 120 | } catch (_) { 121 | return failure(new Error('secretOrPrivateKey is not valid key material')); 122 | } 123 | } 124 | } 125 | 126 | if (header.alg.startsWith('HS') && secretOrPrivateKey.type !== 'secret') { 127 | return failure(new Error((`secretOrPrivateKey must be a symmetric key when using ${header.alg}`))) 128 | } else if (/^(?:RS|PS|ES)/.test(header.alg)) { 129 | if (secretOrPrivateKey.type !== 'private') { 130 | return failure(new Error((`secretOrPrivateKey must be an asymmetric key when using ${header.alg}`))) 131 | } 132 | if (!options.allowInsecureKeySizes && 133 | !header.alg.startsWith('ES') && 134 | secretOrPrivateKey.asymmetricKeyDetails !== undefined && //KeyObject.asymmetricKeyDetails is supported in Node 15+ 135 | secretOrPrivateKey.asymmetricKeyDetails.modulusLength < 2048) { 136 | return failure(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)); 137 | } 138 | } 139 | 140 | if (typeof payload === 'undefined') { 141 | return failure(new Error('payload is required')); 142 | } else if (isObjectPayload) { 143 | try { 144 | validatePayload(payload); 145 | } 146 | catch (error) { 147 | return failure(error); 148 | } 149 | if (!options.mutatePayload) { 150 | payload = Object.assign({},payload); 151 | } 152 | } else { 153 | const invalid_options = options_for_objects.filter(function (opt) { 154 | return typeof options[opt] !== 'undefined'; 155 | }); 156 | 157 | if (invalid_options.length > 0) { 158 | return failure(new Error('invalid ' + invalid_options.join(',') + ' option for ' + (typeof payload ) + ' payload')); 159 | } 160 | } 161 | 162 | if (typeof payload.exp !== 'undefined' && typeof options.expiresIn !== 'undefined') { 163 | return failure(new Error('Bad "options.expiresIn" option the payload already has an "exp" property.')); 164 | } 165 | 166 | if (typeof payload.nbf !== 'undefined' && typeof options.notBefore !== 'undefined') { 167 | return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.')); 168 | } 169 | 170 | try { 171 | validateOptions(options); 172 | } 173 | catch (error) { 174 | return failure(error); 175 | } 176 | 177 | if (!options.allowInvalidAsymmetricKeyTypes) { 178 | try { 179 | validateAsymmetricKey(header.alg, secretOrPrivateKey); 180 | } catch (error) { 181 | return failure(error); 182 | } 183 | } 184 | 185 | const timestamp = payload.iat || Math.floor(Date.now() / 1000); 186 | 187 | if (options.noTimestamp) { 188 | delete payload.iat; 189 | } else if (isObjectPayload) { 190 | payload.iat = timestamp; 191 | } 192 | 193 | if (typeof options.notBefore !== 'undefined') { 194 | try { 195 | payload.nbf = timespan(options.notBefore, timestamp); 196 | } 197 | catch (err) { 198 | return failure(err); 199 | } 200 | if (typeof payload.nbf === 'undefined') { 201 | return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); 202 | } 203 | } 204 | 205 | if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') { 206 | try { 207 | payload.exp = timespan(options.expiresIn, timestamp); 208 | } 209 | catch (err) { 210 | return failure(err); 211 | } 212 | if (typeof payload.exp === 'undefined') { 213 | return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); 214 | } 215 | } 216 | 217 | Object.keys(options_to_payload).forEach(function (key) { 218 | const claim = options_to_payload[key]; 219 | if (typeof options[key] !== 'undefined') { 220 | if (typeof payload[claim] !== 'undefined') { 221 | return failure(new Error('Bad "options.' + key + '" option. The payload already has an "' + claim + '" property.')); 222 | } 223 | payload[claim] = options[key]; 224 | } 225 | }); 226 | 227 | const encoding = options.encoding || 'utf8'; 228 | 229 | if (typeof callback === 'function') { 230 | callback = callback && once(callback); 231 | 232 | jws.createSign({ 233 | header: header, 234 | privateKey: secretOrPrivateKey, 235 | payload: payload, 236 | encoding: encoding 237 | }).once('error', callback) 238 | .once('done', function (signature) { 239 | // TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version 240 | if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) { 241 | return callback(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)) 242 | } 243 | callback(null, signature); 244 | }); 245 | } else { 246 | let signature = jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding}); 247 | // TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version 248 | if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) { 249 | throw new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`) 250 | } 251 | return signature 252 | } 253 | }; 254 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/async_sign.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | var jws = require('jws'); 4 | var PS_SUPPORTED = require('../lib/psSupported'); 5 | const {generateKeyPairSync} = require("crypto"); 6 | 7 | describe('signing a token asynchronously', function() { 8 | 9 | describe('when signing a token', function() { 10 | var secret = 'shhhhhh'; 11 | 12 | it('should return the same result as singing synchronously', function(done) { 13 | jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }, function (err, asyncToken) { 14 | if (err) return done(err); 15 | var syncToken = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }); 16 | expect(asyncToken).to.be.a('string'); 17 | expect(asyncToken.split('.')).to.have.length(3); 18 | expect(asyncToken).to.equal(syncToken); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should work with empty options', function (done) { 24 | jwt.sign({abc: 1}, "secret", {}, function (err) { 25 | expect(err).to.be.null; 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should work without options object at all', function (done) { 31 | jwt.sign({abc: 1}, "secret", function (err) { 32 | expect(err).to.be.null; 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should work with none algorithm where secret is set', function(done) { 38 | jwt.sign({ foo: 'bar' }, 'secret', { algorithm: 'none' }, function(err, token) { 39 | expect(token).to.be.a('string'); 40 | expect(token.split('.')).to.have.length(3); 41 | done(); 42 | }); 43 | }); 44 | 45 | //Known bug: https://github.com/brianloveswords/node-jws/issues/62 46 | //If you need this use case, you need to go for the non-callback-ish code style. 47 | it.skip('should work with none algorithm where secret is falsy', function(done) { 48 | jwt.sign({ foo: 'bar' }, undefined, { algorithm: 'none' }, function(err, token) { 49 | expect(token).to.be.a('string'); 50 | expect(token.split('.')).to.have.length(3); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should return error when secret is not a cert for RS256', function(done) { 56 | //this throw an error because the secret is not a cert and RS256 requires a cert. 57 | jwt.sign({ foo: 'bar' }, secret, { algorithm: 'RS256' }, function (err) { 58 | expect(err).to.be.ok; 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should not work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is false or not set', function(done) { 64 | const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); 65 | 66 | jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function (err) { 67 | expect(err).to.be.ok; 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is true', function(done) { 73 | const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); 74 | 75 | jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256', allowInsecureKeySizes: true }, done); 76 | }); 77 | 78 | if (PS_SUPPORTED) { 79 | it('should return error when secret is not a cert for PS256', function(done) { 80 | //this throw an error because the secret is not a cert and PS256 requires a cert. 81 | jwt.sign({ foo: 'bar' }, secret, { algorithm: 'PS256' }, function (err) { 82 | expect(err).to.be.ok; 83 | done(); 84 | }); 85 | }); 86 | } 87 | 88 | it('should return error on wrong arguments', function(done) { 89 | //this throw an error because the secret is not a cert and RS256 requires a cert. 90 | jwt.sign({ foo: 'bar' }, secret, { notBefore: {} }, function (err) { 91 | expect(err).to.be.ok; 92 | done(); 93 | }); 94 | }); 95 | 96 | it('should return error on wrong arguments (2)', function(done) { 97 | jwt.sign('string', 'secret', {noTimestamp: true}, function (err) { 98 | expect(err).to.be.ok; 99 | expect(err).to.be.instanceof(Error); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should not stringify the payload', function (done) { 105 | jwt.sign('string', 'secret', {}, function (err, token) { 106 | if (err) { return done(err); } 107 | expect(jws.decode(token).payload).to.equal('string'); 108 | done(); 109 | }); 110 | }); 111 | 112 | describe('when mutatePayload is not set', function() { 113 | it('should not apply claims to the original payload object (mutatePayload defaults to false)', function(done) { 114 | var originalPayload = { foo: 'bar' }; 115 | jwt.sign(originalPayload, 'secret', { notBefore: 60, expiresIn: 600 }, function (err) { 116 | if (err) { return done(err); } 117 | expect(originalPayload).to.not.have.property('nbf'); 118 | expect(originalPayload).to.not.have.property('exp'); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('when mutatePayload is set to true', function() { 125 | it('should apply claims directly to the original payload object', function(done) { 126 | var originalPayload = { foo: 'bar' }; 127 | jwt.sign(originalPayload, 'secret', { notBefore: 60, expiresIn: 600, mutatePayload: true }, function (err) { 128 | if (err) { return done(err); } 129 | expect(originalPayload).to.have.property('nbf').that.is.a('number'); 130 | expect(originalPayload).to.have.property('exp').that.is.a('number'); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | 136 | describe('secret must have a value', function(){ 137 | [undefined, '', 0].forEach(function(secret){ 138 | it('should return an error if the secret is falsy and algorithm is not set to none: ' + (typeof secret === 'string' ? '(empty string)' : secret), function(done) { 139 | // This is needed since jws will not answer for falsy secrets 140 | jwt.sign('string', secret, {}, function(err, token) { 141 | expect(err).to.exist; 142 | expect(err.message).to.equal('secretOrPrivateKey must have a value'); 143 | expect(token).to.not.exist; 144 | done(); 145 | }); 146 | }); 147 | }); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/buffer.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require("../."); 2 | var assert = require('chai').assert; 3 | 4 | describe('buffer payload', function () { 5 | it('should work', function () { 6 | var payload = new Buffer('TkJyotZe8NFpgdfnmgINqg==', 'base64'); 7 | var token = jwt.sign(payload, "signing key"); 8 | assert.equal(jwt.decode(token), payload.toString()); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/claim-aud.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const util = require('util'); 6 | const testUtils = require('./test-utils'); 7 | 8 | function signWithAudience(audience, payload, callback) { 9 | const options = {algorithm: 'HS256'}; 10 | if (audience !== undefined) { 11 | options.audience = audience; 12 | } 13 | 14 | testUtils.signJWTHelper(payload, 'secret', options, callback); 15 | } 16 | 17 | function verifyWithAudience(token, audience, callback) { 18 | testUtils.verifyJWTHelper(token, 'secret', {audience}, callback); 19 | } 20 | 21 | describe('audience', function() { 22 | describe('`jwt.sign` "audience" option validation', function () { 23 | [ 24 | true, 25 | false, 26 | null, 27 | -1, 28 | 1, 29 | 0, 30 | -1.1, 31 | 1.1, 32 | -Infinity, 33 | Infinity, 34 | NaN, 35 | {}, 36 | {foo: 'bar'}, 37 | ].forEach((audience) => { 38 | it(`should error with with value ${util.inspect(audience)}`, function (done) { 39 | signWithAudience(audience, {}, (err) => { 40 | testUtils.asyncCheck(done, () => { 41 | expect(err).to.be.instanceOf(Error); 42 | expect(err).to.have.property('message', '"audience" must be a string or array'); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | // undefined needs special treatment because {} is not the same as {aud: undefined} 49 | it('should error with with value undefined', function (done) { 50 | testUtils.signJWTHelper({}, 'secret', {audience: undefined, algorithm: 'HS256'}, (err) => { 51 | testUtils.asyncCheck(done, () => { 52 | expect(err).to.be.instanceOf(Error); 53 | expect(err).to.have.property('message', '"audience" must be a string or array'); 54 | }); 55 | }); 56 | }); 57 | 58 | it('should error when "aud" is in payload', function (done) { 59 | signWithAudience('my_aud', {aud: ''}, (err) => { 60 | testUtils.asyncCheck(done, () => { 61 | expect(err).to.be.instanceOf(Error); 62 | expect(err).to.have.property( 63 | 'message', 64 | 'Bad "options.audience" option. The payload already has an "aud" property.' 65 | ); 66 | }); 67 | }); 68 | }); 69 | 70 | it('should error with a string payload', function (done) { 71 | signWithAudience('my_aud', 'a string payload', (err) => { 72 | testUtils.asyncCheck(done, () => { 73 | expect(err).to.be.instanceOf(Error); 74 | expect(err).to.have.property('message', 'invalid audience option for string payload'); 75 | }); 76 | }); 77 | }); 78 | 79 | it('should error with a Buffer payload', function (done) { 80 | signWithAudience('my_aud', new Buffer('a Buffer payload'), (err) => { 81 | testUtils.asyncCheck(done, () => { 82 | expect(err).to.be.instanceOf(Error); 83 | expect(err).to.have.property('message', 'invalid audience option for object payload'); 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | describe('when signing and verifying a token with "audience" option', function () { 90 | describe('with a "aud" of "urn:foo" in payload', function () { 91 | let token; 92 | 93 | beforeEach(function (done) { 94 | signWithAudience('urn:foo', {}, (err, t) => { 95 | token = t; 96 | done(err); 97 | }); 98 | }); 99 | 100 | [ 101 | undefined, 102 | 'urn:foo', 103 | /^urn:f[o]{2}$/, 104 | ['urn:no_match', 'urn:foo'], 105 | ['urn:no_match', /^urn:f[o]{2}$/], 106 | [/^urn:no_match$/, /^urn:f[o]{2}$/], 107 | [/^urn:no_match$/, 'urn:foo'] 108 | ].forEach((audience) =>{ 109 | it(`should verify and decode with verify "audience" option of ${util.inspect(audience)}`, function (done) { 110 | verifyWithAudience(token, audience, (err, decoded) => { 111 | testUtils.asyncCheck(done, () => { 112 | expect(err).to.be.null; 113 | expect(decoded).to.have.property('aud', 'urn:foo'); 114 | }); 115 | }); 116 | }); 117 | }); 118 | 119 | it(`should error on no match with a string verify "audience" option`, function (done) { 120 | verifyWithAudience(token, 'urn:no-match', (err) => { 121 | testUtils.asyncCheck(done, () => { 122 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 123 | expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match`); 124 | }); 125 | }); 126 | }); 127 | 128 | it('should error on no match with an array of string verify "audience" option', function (done) { 129 | verifyWithAudience(token, ['urn:no-match-1', 'urn:no-match-2'], (err) => { 130 | testUtils.asyncCheck(done, () => { 131 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 132 | expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2`); 133 | }); 134 | }); 135 | }); 136 | 137 | it('should error on no match with a Regex verify "audience" option', function (done) { 138 | verifyWithAudience(token, /^urn:no-match$/, (err) => { 139 | testUtils.asyncCheck(done, () => { 140 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 141 | expect(err).to.have.property('message', `jwt audience invalid. expected: /^urn:no-match$/`); 142 | }); 143 | }); 144 | }); 145 | 146 | it('should error on no match with an array of Regex verify "audience" option', function (done) { 147 | verifyWithAudience(token, [/^urn:no-match-1$/, /^urn:no-match-2$/], (err) => { 148 | testUtils.asyncCheck(done, () => { 149 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 150 | expect(err).to.have.property( 151 | 'message', `jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/` 152 | ); 153 | }); 154 | }); 155 | }); 156 | 157 | it('should error on no match with an array of a Regex and a string in verify "audience" option', function (done) { 158 | verifyWithAudience(token, [/^urn:no-match$/, 'urn:no-match'], (err) => { 159 | testUtils.asyncCheck(done, () => { 160 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 161 | expect(err).to.have.property( 162 | 'message', `jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match` 163 | ); 164 | }); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('with an array of ["urn:foo", "urn:bar"] for "aud" value in payload', function () { 170 | let token; 171 | 172 | beforeEach(function (done) { 173 | signWithAudience(['urn:foo', 'urn:bar'], {}, (err, t) => { 174 | token = t; 175 | done(err); 176 | }); 177 | }); 178 | 179 | [ 180 | undefined, 181 | 'urn:foo', 182 | /^urn:f[o]{2}$/, 183 | ['urn:no_match', 'urn:foo'], 184 | ['urn:no_match', /^urn:f[o]{2}$/], 185 | [/^urn:no_match$/, /^urn:f[o]{2}$/], 186 | [/^urn:no_match$/, 'urn:foo'] 187 | ].forEach((audience) =>{ 188 | it(`should verify and decode with verify "audience" option of ${util.inspect(audience)}`, function (done) { 189 | verifyWithAudience(token, audience, (err, decoded) => { 190 | testUtils.asyncCheck(done, () => { 191 | expect(err).to.be.null; 192 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 193 | }); 194 | }); 195 | }); 196 | }); 197 | 198 | it(`should error on no match with a string verify "audience" option`, function (done) { 199 | verifyWithAudience(token, 'urn:no-match', (err) => { 200 | testUtils.asyncCheck(done, () => { 201 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 202 | expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match`); 203 | }); 204 | }); 205 | }); 206 | 207 | it('should error on no match with an array of string verify "audience" option', function (done) { 208 | verifyWithAudience(token, ['urn:no-match-1', 'urn:no-match-2'], (err) => { 209 | testUtils.asyncCheck(done, () => { 210 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 211 | expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2`); 212 | }); 213 | }); 214 | }); 215 | 216 | it('should error on no match with a Regex verify "audience" option', function (done) { 217 | verifyWithAudience(token, /^urn:no-match$/, (err) => { 218 | testUtils.asyncCheck(done, () => { 219 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 220 | expect(err).to.have.property('message', `jwt audience invalid. expected: /^urn:no-match$/`); 221 | }); 222 | }); 223 | }); 224 | 225 | it('should error on no match with an array of Regex verify "audience" option', function (done) { 226 | verifyWithAudience(token, [/^urn:no-match-1$/, /^urn:no-match-2$/], (err) => { 227 | testUtils.asyncCheck(done, () => { 228 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 229 | expect(err).to.have.property( 230 | 'message', `jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/` 231 | ); 232 | }); 233 | }); 234 | }); 235 | 236 | it('should error on no match with an array of a Regex and a string in verify "audience" option', function (done) { 237 | verifyWithAudience(token, [/^urn:no-match$/, 'urn:no-match'], (err) => { 238 | testUtils.asyncCheck(done, () => { 239 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 240 | expect(err).to.have.property( 241 | 'message', `jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match` 242 | ); 243 | }); 244 | }); 245 | }); 246 | 247 | describe('when checking for a matching on both "urn:foo" and "urn:bar"', function() { 248 | it('should verify with an array of stings verify "audience" option', function (done) { 249 | verifyWithAudience(token, ['urn:foo', 'urn:bar'], (err, decoded) => { 250 | testUtils.asyncCheck(done, () => { 251 | expect(err).to.be.null; 252 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 253 | }); 254 | }); 255 | }); 256 | 257 | it('should verify with a Regex verify "audience" option', function (done) { 258 | verifyWithAudience(token, /^urn:[a-z]{3}$/, (err, decoded) => { 259 | testUtils.asyncCheck(done, () => { 260 | expect(err).to.be.null; 261 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 262 | }); 263 | }); 264 | }); 265 | 266 | it('should verify with an array of Regex verify "audience" option', function (done) { 267 | verifyWithAudience(token, [/^urn:f[o]{2}$/, /^urn:b[ar]{2}$/], (err, decoded) => { 268 | testUtils.asyncCheck(done, () => { 269 | expect(err).to.be.null; 270 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 271 | }); 272 | }); 273 | }); 274 | }); 275 | 276 | describe('when checking for a matching for "urn:foo"', function() { 277 | it('should verify with a string verify "audience"', function (done) { 278 | verifyWithAudience(token, 'urn:foo', (err, decoded) => { 279 | testUtils.asyncCheck(done, () => { 280 | expect(err).to.be.null; 281 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 282 | }); 283 | }); 284 | }); 285 | 286 | it('should verify with a Regex verify "audience" option', function (done) { 287 | verifyWithAudience(token, /^urn:f[o]{2}$/, (err, decoded) => { 288 | testUtils.asyncCheck(done, () => { 289 | expect(err).to.be.null; 290 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 291 | }); 292 | }); 293 | }); 294 | 295 | it('should verify with an array of Regex verify "audience"', function (done) { 296 | verifyWithAudience(token, [/^urn:no-match$/, /^urn:f[o]{2}$/], (err, decoded) => { 297 | testUtils.asyncCheck(done, () => { 298 | expect(err).to.be.null; 299 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 300 | }); 301 | }); 302 | }); 303 | 304 | it('should verify with an array containing a string and a Regex verify "audience" option', function (done) { 305 | verifyWithAudience(token, ['urn:no_match', /^urn:f[o]{2}$/], (err, decoded) => { 306 | testUtils.asyncCheck(done, () => { 307 | expect(err).to.be.null; 308 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 309 | }); 310 | }); 311 | }); 312 | 313 | it('should verify with an array containing a Regex and a string verify "audience" option', function (done) { 314 | verifyWithAudience(token, [/^urn:no-match$/, 'urn:foo'], (err, decoded) => { 315 | testUtils.asyncCheck(done, () => { 316 | expect(err).to.be.null; 317 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 318 | }); 319 | }); 320 | }); 321 | }); 322 | 323 | describe('when checking matching for "urn:bar"', function() { 324 | it('should verify with a string verify "audience"', function (done) { 325 | verifyWithAudience(token, 'urn:bar', (err, decoded) => { 326 | testUtils.asyncCheck(done, () => { 327 | expect(err).to.be.null; 328 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 329 | }); 330 | }); 331 | }); 332 | 333 | it('should verify with a Regex verify "audience" option', function (done) { 334 | verifyWithAudience(token, /^urn:b[ar]{2}$/, (err, decoded) => { 335 | testUtils.asyncCheck(done, () => { 336 | expect(err).to.be.null; 337 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 338 | }); 339 | }); 340 | }); 341 | 342 | it('should verify with an array of Regex verify "audience" option', function (done) { 343 | verifyWithAudience(token, [/^urn:no-match$/, /^urn:b[ar]{2}$/], (err, decoded) => { 344 | testUtils.asyncCheck(done, () => { 345 | expect(err).to.be.null; 346 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 347 | }); 348 | }); 349 | }); 350 | 351 | it('should verify with an array containing a string and a Regex verify "audience" option', function (done) { 352 | verifyWithAudience(token, ['urn:no_match', /^urn:b[ar]{2}$/], (err, decoded) => { 353 | testUtils.asyncCheck(done, () => { 354 | expect(err).to.be.null; 355 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 356 | }); 357 | }); 358 | }); 359 | 360 | it('should verify with an array containing a Regex and a string verify "audience" option', function (done) { 361 | verifyWithAudience(token, [/^urn:no-match$/, 'urn:bar'], (err, decoded) => { 362 | testUtils.asyncCheck(done, () => { 363 | expect(err).to.be.null; 364 | expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); 365 | }); 366 | }); 367 | }); 368 | }); 369 | }); 370 | 371 | describe('without a "aud" value in payload', function () { 372 | let token; 373 | 374 | beforeEach(function (done) { 375 | signWithAudience(undefined, {}, (err, t) => { 376 | token = t; 377 | done(err); 378 | }); 379 | }); 380 | 381 | it('should verify and decode without verify "audience" option', function (done) { 382 | verifyWithAudience(token, undefined, (err, decoded) => { 383 | testUtils.asyncCheck(done, () => { 384 | expect(err).to.be.null; 385 | expect(decoded).to.not.have.property('aud'); 386 | }); 387 | }); 388 | }); 389 | 390 | it('should error on no match with a string verify "audience" option', function (done) { 391 | verifyWithAudience(token, 'urn:no-match', (err) => { 392 | testUtils.asyncCheck(done, () => { 393 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 394 | expect(err).to.have.property('message', 'jwt audience invalid. expected: urn:no-match'); 395 | }); 396 | }); 397 | }); 398 | 399 | it('should error on no match with an array of string verify "audience" option', function (done) { 400 | verifyWithAudience(token, ['urn:no-match-1', 'urn:no-match-2'], (err) => { 401 | testUtils.asyncCheck(done, () => { 402 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 403 | expect(err).to.have.property('message', 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); 404 | }); 405 | }); 406 | }); 407 | 408 | it('should error on no match with a Regex verify "audience" option', function (done) { 409 | verifyWithAudience(token, /^urn:no-match$/, (err) => { 410 | testUtils.asyncCheck(done, () => { 411 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 412 | expect(err).to.have.property('message', 'jwt audience invalid. expected: /^urn:no-match$/'); 413 | }); 414 | }); 415 | }); 416 | 417 | it('should error on no match with an array of Regex verify "audience" option', function (done) { 418 | verifyWithAudience(token, [/^urn:no-match-1$/, /^urn:no-match-2$/], (err) => { 419 | testUtils.asyncCheck(done, () => { 420 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 421 | expect(err).to.have.property('message', 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); 422 | }); 423 | }); 424 | }); 425 | 426 | it('should error on no match with an array of a Regex and a string in verify "audience" option', function (done) { 427 | verifyWithAudience(token, [/^urn:no-match$/, 'urn:no-match'], (err) => { 428 | testUtils.asyncCheck(done, () => { 429 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 430 | expect(err).to.have.property('message', 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); 431 | }); 432 | }); 433 | }); 434 | }); 435 | }); 436 | }); 437 | -------------------------------------------------------------------------------- /test/claim-exp.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | const util = require('util'); 7 | const testUtils = require('./test-utils'); 8 | const jws = require('jws'); 9 | 10 | function signWithExpiresIn(expiresIn, payload, callback) { 11 | const options = {algorithm: 'HS256'}; 12 | if (expiresIn !== undefined) { 13 | options.expiresIn = expiresIn; 14 | } 15 | testUtils.signJWTHelper(payload, 'secret', options, callback); 16 | } 17 | 18 | describe('expires', function() { 19 | describe('`jwt.sign` "expiresIn" option validation', function () { 20 | [ 21 | true, 22 | false, 23 | null, 24 | -1.1, 25 | 1.1, 26 | -Infinity, 27 | Infinity, 28 | NaN, 29 | ' ', 30 | '', 31 | 'invalid', 32 | [], 33 | ['foo'], 34 | {}, 35 | {foo: 'bar'}, 36 | ].forEach((expiresIn) => { 37 | it(`should error with with value ${util.inspect(expiresIn)}`, function (done) { 38 | signWithExpiresIn(expiresIn, {}, (err) => { 39 | testUtils.asyncCheck(done, () => { 40 | expect(err).to.be.instanceOf(Error); 41 | expect(err).to.have.property('message') 42 | .match(/"expiresIn" should be a number of seconds or string representing a timespan/); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | // undefined needs special treatment because {} is not the same as {expiresIn: undefined} 49 | it('should error with with value undefined', function (done) { 50 | testUtils.signJWTHelper({}, 'secret', {expiresIn: undefined, algorithm: 'HS256'}, (err) => { 51 | testUtils.asyncCheck(done, () => { 52 | expect(err).to.be.instanceOf(Error); 53 | expect(err).to.have.property( 54 | 'message', 55 | '"expiresIn" should be a number of seconds or string representing a timespan' 56 | ); 57 | }); 58 | }); 59 | }); 60 | 61 | it ('should error when "exp" is in payload', function(done) { 62 | signWithExpiresIn(100, {exp: 100}, (err) => { 63 | testUtils.asyncCheck(done, () => { 64 | expect(err).to.be.instanceOf(Error); 65 | expect(err).to.have.property( 66 | 'message', 67 | 'Bad "options.expiresIn" option the payload already has an "exp" property.' 68 | ); 69 | }); 70 | }); 71 | }); 72 | 73 | it('should error with a string payload', function(done) { 74 | signWithExpiresIn(100, 'a string payload', (err) => { 75 | testUtils.asyncCheck(done, () => { 76 | expect(err).to.be.instanceOf(Error); 77 | expect(err).to.have.property('message', 'invalid expiresIn option for string payload'); 78 | }); 79 | }); 80 | }); 81 | 82 | it('should error with a Buffer payload', function(done) { 83 | signWithExpiresIn(100, Buffer.from('a Buffer payload'), (err) => { 84 | testUtils.asyncCheck(done, () => { 85 | expect(err).to.be.instanceOf(Error); 86 | expect(err).to.have.property('message', 'invalid expiresIn option for object payload'); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('`jwt.sign` "exp" claim validation', function () { 93 | [ 94 | true, 95 | false, 96 | null, 97 | undefined, 98 | '', 99 | ' ', 100 | 'invalid', 101 | [], 102 | ['foo'], 103 | {}, 104 | {foo: 'bar'}, 105 | ].forEach((exp) => { 106 | it(`should error with with value ${util.inspect(exp)}`, function (done) { 107 | signWithExpiresIn(undefined, {exp}, (err) => { 108 | testUtils.asyncCheck(done, () => { 109 | expect(err).to.be.instanceOf(Error); 110 | expect(err).to.have.property('message', '"exp" should be a number of seconds'); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('"exp" in payload validation', function () { 118 | [ 119 | true, 120 | false, 121 | null, 122 | -Infinity, 123 | Infinity, 124 | NaN, 125 | '', 126 | ' ', 127 | 'invalid', 128 | [], 129 | ['foo'], 130 | {}, 131 | {foo: 'bar'}, 132 | ].forEach((exp) => { 133 | it(`should error with with value ${util.inspect(exp)}`, function (done) { 134 | const header = { alg: 'HS256' }; 135 | const payload = { exp }; 136 | const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); 137 | testUtils.verifyJWTHelper(token, 'secret', { exp }, (err) => { 138 | testUtils.asyncCheck(done, () => { 139 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 140 | expect(err).to.have.property('message', 'invalid exp value'); 141 | }); 142 | }); 143 | }); 144 | }) 145 | }); 146 | 147 | describe('when signing and verifying a token with expires option', function () { 148 | let fakeClock; 149 | beforeEach(function() { 150 | fakeClock = sinon.useFakeTimers({now: 60000}); 151 | }); 152 | 153 | afterEach(function() { 154 | fakeClock.uninstall(); 155 | }); 156 | 157 | it('should set correct "exp" with negative number of seconds', function(done) { 158 | signWithExpiresIn(-10, {}, (e1, token) => { 159 | fakeClock.tick(-10001); 160 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 161 | testUtils.asyncCheck(done, () => { 162 | expect(e1).to.be.null; 163 | expect(e2).to.be.null; 164 | expect(decoded).to.have.property('exp', 50); 165 | }); 166 | }) 167 | }); 168 | }); 169 | 170 | it('should set correct "exp" with positive number of seconds', function(done) { 171 | signWithExpiresIn(10, {}, (e1, token) => { 172 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 173 | testUtils.asyncCheck(done, () => { 174 | expect(e1).to.be.null; 175 | expect(e2).to.be.null; 176 | expect(decoded).to.have.property('exp', 70); 177 | }); 178 | }) 179 | }); 180 | }); 181 | 182 | it('should set correct "exp" with zero seconds', function(done) { 183 | signWithExpiresIn(0, {}, (e1, token) => { 184 | fakeClock.tick(-1); 185 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 186 | testUtils.asyncCheck(done, () => { 187 | expect(e1).to.be.null; 188 | expect(e2).to.be.null; 189 | expect(decoded).to.have.property('exp', 60); 190 | }); 191 | }) 192 | }); 193 | }); 194 | 195 | it('should set correct "exp" with negative string timespan', function(done) { 196 | signWithExpiresIn('-10 s', {}, (e1, token) => { 197 | fakeClock.tick(-10001); 198 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 199 | testUtils.asyncCheck(done, () => { 200 | expect(e1).to.be.null; 201 | expect(e2).to.be.null; 202 | expect(decoded).to.have.property('exp', 50); 203 | }); 204 | }) 205 | }); 206 | }); 207 | 208 | it('should set correct "exp" with positive string timespan', function(done) { 209 | signWithExpiresIn('10 s', {}, (e1, token) => { 210 | fakeClock.tick(-10001); 211 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 212 | testUtils.asyncCheck(done, () => { 213 | expect(e1).to.be.null; 214 | expect(e2).to.be.null; 215 | expect(decoded).to.have.property('exp', 70); 216 | }); 217 | }) 218 | }); 219 | }); 220 | 221 | it('should set correct "exp" with zero string timespan', function(done) { 222 | signWithExpiresIn('0 s', {}, (e1, token) => { 223 | fakeClock.tick(-1); 224 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 225 | testUtils.asyncCheck(done, () => { 226 | expect(e1).to.be.null; 227 | expect(e2).to.be.null; 228 | expect(decoded).to.have.property('exp', 60); 229 | }); 230 | }) 231 | }); 232 | }); 233 | 234 | // TODO an exp of -Infinity should fail validation 235 | it('should set null "exp" when given -Infinity', function (done) { 236 | signWithExpiresIn(undefined, {exp: -Infinity}, (err, token) => { 237 | const decoded = jwt.decode(token); 238 | testUtils.asyncCheck(done, () => { 239 | expect(err).to.be.null; 240 | expect(decoded).to.have.property('exp', null); 241 | }); 242 | }); 243 | }); 244 | 245 | // TODO an exp of Infinity should fail validation 246 | it('should set null "exp" when given value Infinity', function (done) { 247 | signWithExpiresIn(undefined, {exp: Infinity}, (err, token) => { 248 | const decoded = jwt.decode(token); 249 | testUtils.asyncCheck(done, () => { 250 | expect(err).to.be.null; 251 | expect(decoded).to.have.property('exp', null); 252 | }); 253 | }); 254 | }); 255 | 256 | // TODO an exp of NaN should fail validation 257 | it('should set null "exp" when given value NaN', function (done) { 258 | signWithExpiresIn(undefined, {exp: NaN}, (err, token) => { 259 | const decoded = jwt.decode(token); 260 | testUtils.asyncCheck(done, () => { 261 | expect(err).to.be.null; 262 | expect(decoded).to.have.property('exp', null); 263 | }); 264 | }); 265 | }); 266 | 267 | it('should set correct "exp" when "iat" is passed', function (done) { 268 | signWithExpiresIn(-10, {iat: 80}, (e1, token) => { 269 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 270 | testUtils.asyncCheck(done, () => { 271 | expect(e1).to.be.null; 272 | expect(e2).to.be.null; 273 | expect(decoded).to.have.property('exp', 70); 274 | }); 275 | }) 276 | }); 277 | }); 278 | 279 | it('should verify "exp" using "clockTimestamp"', function (done) { 280 | signWithExpiresIn(10, {}, (e1, token) => { 281 | testUtils.verifyJWTHelper(token, 'secret', {clockTimestamp: 69}, (e2, decoded) => { 282 | testUtils.asyncCheck(done, () => { 283 | expect(e1).to.be.null; 284 | expect(e2).to.be.null; 285 | expect(decoded).to.have.property('iat', 60); 286 | expect(decoded).to.have.property('exp', 70); 287 | }); 288 | }) 289 | }); 290 | }); 291 | 292 | it('should verify "exp" using "clockTolerance"', function (done) { 293 | signWithExpiresIn(5, {}, (e1, token) => { 294 | fakeClock.tick(10000); 295 | testUtils.verifyJWTHelper(token, 'secret', {clockTimestamp: 6}, (e2, decoded) => { 296 | testUtils.asyncCheck(done, () => { 297 | expect(e1).to.be.null; 298 | expect(e2).to.be.null; 299 | expect(decoded).to.have.property('iat', 60); 300 | expect(decoded).to.have.property('exp', 65); 301 | }); 302 | }) 303 | }); 304 | }); 305 | 306 | it('should ignore a expired token when "ignoreExpiration" is true', function (done) { 307 | signWithExpiresIn('-10 s', {}, (e1, token) => { 308 | testUtils.verifyJWTHelper(token, 'secret', {ignoreExpiration: true}, (e2, decoded) => { 309 | testUtils.asyncCheck(done, () => { 310 | expect(e1).to.be.null; 311 | expect(e2).to.be.null; 312 | expect(decoded).to.have.property('iat', 60); 313 | expect(decoded).to.have.property('exp', 50); 314 | }); 315 | }) 316 | }); 317 | }); 318 | 319 | it('should error on verify if "exp" is at current time', function(done) { 320 | signWithExpiresIn(undefined, {exp: 60}, (e1, token) => { 321 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2) => { 322 | testUtils.asyncCheck(done, () => { 323 | expect(e1).to.be.null; 324 | expect(e2).to.be.instanceOf(jwt.TokenExpiredError); 325 | expect(e2).to.have.property('message', 'jwt expired'); 326 | }); 327 | }); 328 | }); 329 | }); 330 | 331 | it('should error on verify if "exp" is before current time using clockTolerance', function (done) { 332 | signWithExpiresIn(-5, {}, (e1, token) => { 333 | testUtils.verifyJWTHelper(token, 'secret', {clockTolerance: 5}, (e2) => { 334 | testUtils.asyncCheck(done, () => { 335 | expect(e1).to.be.null; 336 | expect(e2).to.be.instanceOf(jwt.TokenExpiredError); 337 | expect(e2).to.have.property('message', 'jwt expired'); 338 | }); 339 | }); 340 | }); 341 | }); 342 | }); 343 | }); 344 | -------------------------------------------------------------------------------- /test/claim-iat.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | const util = require('util'); 7 | const testUtils = require('./test-utils'); 8 | const jws = require('jws'); 9 | 10 | function signWithIssueAt(issueAt, options, callback) { 11 | const payload = {}; 12 | if (issueAt !== undefined) { 13 | payload.iat = issueAt; 14 | } 15 | const opts = Object.assign({algorithm: 'HS256'}, options); 16 | // async calls require a truthy secret 17 | // see: https://github.com/brianloveswords/node-jws/issues/62 18 | testUtils.signJWTHelper(payload, 'secret', opts, callback); 19 | } 20 | 21 | function verifyWithIssueAt(token, maxAge, options, secret, callback) { 22 | const opts = Object.assign({maxAge}, options); 23 | testUtils.verifyJWTHelper(token, secret, opts, callback); 24 | } 25 | 26 | describe('issue at', function() { 27 | describe('`jwt.sign` "iat" claim validation', function () { 28 | [ 29 | true, 30 | false, 31 | null, 32 | '', 33 | 'invalid', 34 | [], 35 | ['foo'], 36 | {}, 37 | {foo: 'bar'}, 38 | ].forEach((iat) => { 39 | it(`should error with iat of ${util.inspect(iat)}`, function (done) { 40 | signWithIssueAt(iat, {}, (err) => { 41 | testUtils.asyncCheck(done, () => { 42 | expect(err).to.be.instanceOf(Error); 43 | expect(err.message).to.equal('"iat" should be a number of seconds'); 44 | }); 45 | }); 46 | }); 47 | }); 48 | 49 | // undefined needs special treatment because {} is not the same as {iat: undefined} 50 | it('should error with iat of undefined', function (done) { 51 | testUtils.signJWTHelper({iat: undefined}, 'secret', {algorithm: 'HS256'}, (err) => { 52 | testUtils.asyncCheck(done, () => { 53 | expect(err).to.be.instanceOf(Error); 54 | expect(err.message).to.equal('"iat" should be a number of seconds'); 55 | }); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('"iat" in payload with "maxAge" option validation', function () { 61 | [ 62 | true, 63 | false, 64 | null, 65 | undefined, 66 | -Infinity, 67 | Infinity, 68 | NaN, 69 | '', 70 | 'invalid', 71 | [], 72 | ['foo'], 73 | {}, 74 | {foo: 'bar'}, 75 | ].forEach((iat) => { 76 | it(`should error with iat of ${util.inspect(iat)}`, function (done) { 77 | const header = { alg: 'HS256' }; 78 | const payload = { iat }; 79 | const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); 80 | verifyWithIssueAt(token, '1 min', {}, 'secret', (err) => { 81 | testUtils.asyncCheck(done, () => { 82 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 83 | expect(err.message).to.equal('iat required when maxAge is specified'); 84 | }); 85 | }); 86 | }); 87 | }) 88 | }); 89 | 90 | describe('when signing a token', function () { 91 | let fakeClock; 92 | beforeEach(function () { 93 | fakeClock = sinon.useFakeTimers({now: 60000}); 94 | }); 95 | 96 | afterEach(function () { 97 | fakeClock.uninstall(); 98 | }); 99 | 100 | [ 101 | { 102 | description: 'should default to current time for "iat"', 103 | iat: undefined, 104 | expectedIssueAt: 60, 105 | options: {} 106 | }, 107 | { 108 | description: 'should sign with provided time for "iat"', 109 | iat: 100, 110 | expectedIssueAt: 100, 111 | options: {} 112 | }, 113 | // TODO an iat of -Infinity should fail validation 114 | { 115 | description: 'should set null "iat" when given -Infinity', 116 | iat: -Infinity, 117 | expectedIssueAt: null, 118 | options: {} 119 | }, 120 | // TODO an iat of Infinity should fail validation 121 | { 122 | description: 'should set null "iat" when given Infinity', 123 | iat: Infinity, 124 | expectedIssueAt: null, 125 | options: {} 126 | }, 127 | // TODO an iat of NaN should fail validation 128 | { 129 | description: 'should set to current time for "iat" when given value NaN', 130 | iat: NaN, 131 | expectedIssueAt: 60, 132 | options: {} 133 | }, 134 | { 135 | description: 'should remove default "iat" with "noTimestamp" option', 136 | iat: undefined, 137 | expectedIssueAt: undefined, 138 | options: {noTimestamp: true} 139 | }, 140 | { 141 | description: 'should remove provided "iat" with "noTimestamp" option', 142 | iat: 10, 143 | expectedIssueAt: undefined, 144 | options: {noTimestamp: true} 145 | }, 146 | ].forEach((testCase) => { 147 | it(testCase.description, function (done) { 148 | signWithIssueAt(testCase.iat, testCase.options, (err, token) => { 149 | testUtils.asyncCheck(done, () => { 150 | expect(err).to.be.null; 151 | expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); 152 | }); 153 | }); 154 | }); 155 | }); 156 | }); 157 | 158 | describe('when verifying a token', function() { 159 | let fakeClock; 160 | 161 | beforeEach(function() { 162 | fakeClock = sinon.useFakeTimers({now: 60000}); 163 | }); 164 | 165 | afterEach(function () { 166 | fakeClock.uninstall(); 167 | }); 168 | 169 | [ 170 | { 171 | description: 'should verify using "iat" before the "maxAge"', 172 | clockAdvance: 10000, 173 | maxAge: 11, 174 | options: {}, 175 | }, 176 | { 177 | description: 'should verify using "iat" before the "maxAge" with a provided "clockTimestamp', 178 | clockAdvance: 60000, 179 | maxAge: 11, 180 | options: {clockTimestamp: 70}, 181 | }, 182 | { 183 | description: 'should verify using "iat" after the "maxAge" but within "clockTolerance"', 184 | clockAdvance: 10000, 185 | maxAge: 9, 186 | options: {clockTimestamp: 2}, 187 | }, 188 | ].forEach((testCase) => { 189 | it(testCase.description, function (done) { 190 | const token = jwt.sign({}, 'secret', {algorithm: 'HS256'}); 191 | fakeClock.tick(testCase.clockAdvance); 192 | verifyWithIssueAt(token, testCase.maxAge, testCase.options, 'secret', (err, token) => { 193 | testUtils.asyncCheck(done, () => { 194 | expect(err).to.be.null; 195 | expect(token).to.be.a('object'); 196 | }); 197 | }); 198 | }); 199 | }); 200 | 201 | [ 202 | { 203 | description: 'should throw using "iat" equal to the "maxAge"', 204 | clockAdvance: 10000, 205 | maxAge: 10, 206 | options: {}, 207 | expectedError: 'maxAge exceeded', 208 | expectedExpiresAt: 70000, 209 | }, 210 | { 211 | description: 'should throw using "iat" after the "maxAge"', 212 | clockAdvance: 10000, 213 | maxAge: 9, 214 | options: {}, 215 | expectedError: 'maxAge exceeded', 216 | expectedExpiresAt: 69000, 217 | }, 218 | { 219 | description: 'should throw using "iat" after the "maxAge" with a provided "clockTimestamp', 220 | clockAdvance: 60000, 221 | maxAge: 10, 222 | options: {clockTimestamp: 70}, 223 | expectedError: 'maxAge exceeded', 224 | expectedExpiresAt: 70000, 225 | }, 226 | { 227 | description: 'should throw using "iat" after the "maxAge" and "clockTolerance', 228 | clockAdvance: 10000, 229 | maxAge: 8, 230 | options: {clockTolerance: 2}, 231 | expectedError: 'maxAge exceeded', 232 | expectedExpiresAt: 68000, 233 | }, 234 | ].forEach((testCase) => { 235 | it(testCase.description, function(done) { 236 | const expectedExpiresAtDate = new Date(testCase.expectedExpiresAt); 237 | const token = jwt.sign({}, 'secret', {algorithm: 'HS256'}); 238 | fakeClock.tick(testCase.clockAdvance); 239 | 240 | verifyWithIssueAt(token, testCase.maxAge, testCase.options, 'secret', (err) => { 241 | testUtils.asyncCheck(done, () => { 242 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 243 | expect(err.message).to.equal(testCase.expectedError); 244 | expect(err.expiredAt).to.deep.equal(expectedExpiresAtDate); 245 | }); 246 | }); 247 | }); 248 | }); 249 | }); 250 | 251 | describe('with string payload', function () { 252 | it('should not add iat to string', function (done) { 253 | const payload = 'string payload'; 254 | const options = {algorithm: 'HS256'}; 255 | testUtils.signJWTHelper(payload, 'secret', options, (err, token) => { 256 | const decoded = jwt.decode(token); 257 | testUtils.asyncCheck(done, () => { 258 | expect(err).to.be.null; 259 | expect(decoded).to.equal(payload); 260 | }); 261 | }); 262 | }); 263 | 264 | it('should not add iat to stringified object', function (done) { 265 | const payload = '{}'; 266 | const options = {algorithm: 'HS256', header: {typ: 'JWT'}}; 267 | testUtils.signJWTHelper(payload, 'secret', options, (err, token) => { 268 | const decoded = jwt.decode(token); 269 | testUtils.asyncCheck(done, () => { 270 | expect(err).to.equal(null); 271 | expect(JSON.stringify(decoded)).to.equal(payload); 272 | }); 273 | }); 274 | }); 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /test/claim-iss.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const util = require('util'); 6 | const testUtils = require('./test-utils'); 7 | 8 | function signWithIssuer(issuer, payload, callback) { 9 | const options = {algorithm: 'HS256'}; 10 | if (issuer !== undefined) { 11 | options.issuer = issuer; 12 | } 13 | testUtils.signJWTHelper(payload, 'secret', options, callback); 14 | } 15 | 16 | describe('issuer', function() { 17 | describe('`jwt.sign` "issuer" option validation', function () { 18 | [ 19 | true, 20 | false, 21 | null, 22 | -1, 23 | 0, 24 | 1, 25 | -1.1, 26 | 1.1, 27 | -Infinity, 28 | Infinity, 29 | NaN, 30 | [], 31 | ['foo'], 32 | {}, 33 | {foo: 'bar'}, 34 | ].forEach((issuer) => { 35 | it(`should error with with value ${util.inspect(issuer)}`, function (done) { 36 | signWithIssuer(issuer, {}, (err) => { 37 | testUtils.asyncCheck(done, () => { 38 | expect(err).to.be.instanceOf(Error); 39 | expect(err).to.have.property('message', '"issuer" must be a string'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | // undefined needs special treatment because {} is not the same as {issuer: undefined} 46 | it('should error with with value undefined', function (done) { 47 | testUtils.signJWTHelper({}, 'secret', {issuer: undefined, algorithm: 'HS256'}, (err) => { 48 | testUtils.asyncCheck(done, () => { 49 | expect(err).to.be.instanceOf(Error); 50 | expect(err).to.have.property('message', '"issuer" must be a string'); 51 | }); 52 | }); 53 | }); 54 | 55 | it('should error when "iss" is in payload', function (done) { 56 | signWithIssuer('foo', {iss: 'bar'}, (err) => { 57 | testUtils.asyncCheck(done, () => { 58 | expect(err).to.be.instanceOf(Error); 59 | expect(err).to.have.property( 60 | 'message', 61 | 'Bad "options.issuer" option. The payload already has an "iss" property.' 62 | ); 63 | }); 64 | }); 65 | }); 66 | 67 | it('should error with a string payload', function (done) { 68 | signWithIssuer('foo', 'a string payload', (err) => { 69 | testUtils.asyncCheck(done, () => { 70 | expect(err).to.be.instanceOf(Error); 71 | expect(err).to.have.property( 72 | 'message', 73 | 'invalid issuer option for string payload' 74 | ); 75 | }); 76 | }); 77 | }); 78 | 79 | it('should error with a Buffer payload', function (done) { 80 | signWithIssuer('foo', new Buffer('a Buffer payload'), (err) => { 81 | testUtils.asyncCheck(done, () => { 82 | expect(err).to.be.instanceOf(Error); 83 | expect(err).to.have.property( 84 | 'message', 85 | 'invalid issuer option for object payload' 86 | ); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('when signing and verifying a token', function () { 93 | it('should not verify "iss" if verify "issuer" option not provided', function(done) { 94 | signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { 95 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 96 | testUtils.asyncCheck(done, () => { 97 | expect(e1).to.be.null; 98 | expect(e2).to.be.null; 99 | expect(decoded).to.have.property('iss', 'foo'); 100 | }); 101 | }) 102 | }); 103 | }); 104 | 105 | describe('with string "issuer" option', function () { 106 | it('should verify with a string "issuer"', function (done) { 107 | signWithIssuer('foo', {}, (e1, token) => { 108 | testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2, decoded) => { 109 | testUtils.asyncCheck(done, () => { 110 | expect(e1).to.be.null; 111 | expect(e2).to.be.null; 112 | expect(decoded).to.have.property('iss', 'foo'); 113 | }); 114 | }) 115 | }); 116 | }); 117 | 118 | it('should verify with a string "iss"', function (done) { 119 | signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { 120 | testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2, decoded) => { 121 | testUtils.asyncCheck(done, () => { 122 | expect(e1).to.be.null; 123 | expect(e2).to.be.null; 124 | expect(decoded).to.have.property('iss', 'foo'); 125 | }); 126 | }) 127 | }); 128 | }); 129 | 130 | it('should error if "iss" does not match verify "issuer" option', function(done) { 131 | signWithIssuer(undefined, {iss: 'foobar'}, (e1, token) => { 132 | testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2) => { 133 | testUtils.asyncCheck(done, () => { 134 | expect(e1).to.be.null; 135 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 136 | expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo'); 137 | }); 138 | }) 139 | }); 140 | }); 141 | 142 | it('should error without "iss" and with verify "issuer" option', function(done) { 143 | signWithIssuer(undefined, {}, (e1, token) => { 144 | testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2) => { 145 | testUtils.asyncCheck(done, () => { 146 | expect(e1).to.be.null; 147 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 148 | expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo'); 149 | }); 150 | }) 151 | }); 152 | }); 153 | }); 154 | 155 | describe('with array "issuer" option', function () { 156 | it('should verify with a string "issuer"', function (done) { 157 | signWithIssuer('bar', {}, (e1, token) => { 158 | testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2, decoded) => { 159 | testUtils.asyncCheck(done, () => { 160 | expect(e1).to.be.null; 161 | expect(e2).to.be.null; 162 | expect(decoded).to.have.property('iss', 'bar'); 163 | }); 164 | }) 165 | }); 166 | }); 167 | 168 | it('should verify with a string "iss"', function (done) { 169 | signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { 170 | testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2, decoded) => { 171 | testUtils.asyncCheck(done, () => { 172 | expect(e1).to.be.null; 173 | expect(e2).to.be.null; 174 | expect(decoded).to.have.property('iss', 'foo'); 175 | }); 176 | }) 177 | }); 178 | }); 179 | 180 | it('should error if "iss" does not match verify "issuer" option', function(done) { 181 | signWithIssuer(undefined, {iss: 'foobar'}, (e1, token) => { 182 | testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2) => { 183 | testUtils.asyncCheck(done, () => { 184 | expect(e1).to.be.null; 185 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 186 | expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo,bar'); 187 | }); 188 | }) 189 | }); 190 | }); 191 | 192 | it('should error without "iss" and with verify "issuer" option', function(done) { 193 | signWithIssuer(undefined, {}, (e1, token) => { 194 | testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2) => { 195 | testUtils.asyncCheck(done, () => { 196 | expect(e1).to.be.null; 197 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 198 | expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo,bar'); 199 | }); 200 | }) 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /test/claim-jti.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const util = require('util'); 6 | const testUtils = require('./test-utils'); 7 | 8 | function signWithJWTId(jwtid, payload, callback) { 9 | const options = {algorithm: 'HS256'}; 10 | if (jwtid !== undefined) { 11 | options.jwtid = jwtid; 12 | } 13 | testUtils.signJWTHelper(payload, 'secret', options, callback); 14 | } 15 | 16 | describe('jwtid', function() { 17 | describe('`jwt.sign` "jwtid" option validation', function () { 18 | [ 19 | true, 20 | false, 21 | null, 22 | -1, 23 | 0, 24 | 1, 25 | -1.1, 26 | 1.1, 27 | -Infinity, 28 | Infinity, 29 | NaN, 30 | [], 31 | ['foo'], 32 | {}, 33 | {foo: 'bar'}, 34 | ].forEach((jwtid) => { 35 | it(`should error with with value ${util.inspect(jwtid)}`, function (done) { 36 | signWithJWTId(jwtid, {}, (err) => { 37 | testUtils.asyncCheck(done, () => { 38 | expect(err).to.be.instanceOf(Error); 39 | expect(err).to.have.property('message', '"jwtid" must be a string'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | // undefined needs special treatment because {} is not the same as {jwtid: undefined} 46 | it('should error with with value undefined', function (done) { 47 | testUtils.signJWTHelper({}, 'secret', {jwtid: undefined, algorithm: 'HS256'}, (err) => { 48 | testUtils.asyncCheck(done, () => { 49 | expect(err).to.be.instanceOf(Error); 50 | expect(err).to.have.property('message', '"jwtid" must be a string'); 51 | }); 52 | }); 53 | }); 54 | 55 | it('should error when "jti" is in payload', function (done) { 56 | signWithJWTId('foo', {jti: 'bar'}, (err) => { 57 | testUtils.asyncCheck(done, () => { 58 | expect(err).to.be.instanceOf(Error); 59 | expect(err).to.have.property( 60 | 'message', 61 | 'Bad "options.jwtid" option. The payload already has an "jti" property.' 62 | ); 63 | }); 64 | }); 65 | }); 66 | 67 | it('should error with a string payload', function (done) { 68 | signWithJWTId('foo', 'a string payload', (err) => { 69 | testUtils.asyncCheck(done, () => { 70 | expect(err).to.be.instanceOf(Error); 71 | expect(err).to.have.property( 72 | 'message', 73 | 'invalid jwtid option for string payload' 74 | ); 75 | }); 76 | }); 77 | }); 78 | 79 | it('should error with a Buffer payload', function (done) { 80 | signWithJWTId('foo', new Buffer('a Buffer payload'), (err) => { 81 | testUtils.asyncCheck(done, () => { 82 | expect(err).to.be.instanceOf(Error); 83 | expect(err).to.have.property( 84 | 'message', 85 | 'invalid jwtid option for object payload' 86 | ); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('when signing and verifying a token', function () { 93 | it('should not verify "jti" if verify "jwtid" option not provided', function(done) { 94 | signWithJWTId(undefined, {jti: 'foo'}, (e1, token) => { 95 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 96 | testUtils.asyncCheck(done, () => { 97 | expect(e1).to.be.null; 98 | expect(e2).to.be.null; 99 | expect(decoded).to.have.property('jti', 'foo'); 100 | }); 101 | }) 102 | }); 103 | }); 104 | 105 | describe('with "jwtid" option', function () { 106 | it('should verify with "jwtid" option', function (done) { 107 | signWithJWTId('foo', {}, (e1, token) => { 108 | testUtils.verifyJWTHelper(token, 'secret', {jwtid: 'foo'}, (e2, decoded) => { 109 | testUtils.asyncCheck(done, () => { 110 | expect(e1).to.be.null; 111 | expect(e2).to.be.null; 112 | expect(decoded).to.have.property('jti', 'foo'); 113 | }); 114 | }) 115 | }); 116 | }); 117 | 118 | it('should verify with "jti" in payload', function (done) { 119 | signWithJWTId(undefined, {jti: 'foo'}, (e1, token) => { 120 | testUtils.verifyJWTHelper(token, 'secret', {jetid: 'foo'}, (e2, decoded) => { 121 | testUtils.asyncCheck(done, () => { 122 | expect(e1).to.be.null; 123 | expect(e2).to.be.null; 124 | expect(decoded).to.have.property('jti', 'foo'); 125 | }); 126 | }) 127 | }); 128 | }); 129 | 130 | it('should error if "jti" does not match verify "jwtid" option', function(done) { 131 | signWithJWTId(undefined, {jti: 'bar'}, (e1, token) => { 132 | testUtils.verifyJWTHelper(token, 'secret', {jwtid: 'foo'}, (e2) => { 133 | testUtils.asyncCheck(done, () => { 134 | expect(e1).to.be.null; 135 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 136 | expect(e2).to.have.property('message', 'jwt jwtid invalid. expected: foo'); 137 | }); 138 | }) 139 | }); 140 | }); 141 | 142 | it('should error without "jti" and with verify "jwtid" option', function(done) { 143 | signWithJWTId(undefined, {}, (e1, token) => { 144 | testUtils.verifyJWTHelper(token, 'secret', {jwtid: 'foo'}, (e2) => { 145 | testUtils.asyncCheck(done, () => { 146 | expect(e1).to.be.null; 147 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 148 | expect(e2).to.have.property('message', 'jwt jwtid invalid. expected: foo'); 149 | }); 150 | }) 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/claim-nbf.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | const util = require('util'); 7 | const testUtils = require('./test-utils'); 8 | const jws = require('jws'); 9 | 10 | function signWithNotBefore(notBefore, payload, callback) { 11 | const options = {algorithm: 'HS256'}; 12 | if (notBefore !== undefined) { 13 | options.notBefore = notBefore; 14 | } 15 | testUtils.signJWTHelper(payload, 'secret', options, callback); 16 | } 17 | 18 | describe('not before', function() { 19 | describe('`jwt.sign` "notBefore" option validation', function () { 20 | [ 21 | true, 22 | false, 23 | null, 24 | -1.1, 25 | 1.1, 26 | -Infinity, 27 | Infinity, 28 | NaN, 29 | '', 30 | ' ', 31 | 'invalid', 32 | [], 33 | ['foo'], 34 | {}, 35 | {foo: 'bar'}, 36 | ].forEach((notBefore) => { 37 | it(`should error with with value ${util.inspect(notBefore)}`, function (done) { 38 | signWithNotBefore(notBefore, {}, (err) => { 39 | testUtils.asyncCheck(done, () => { 40 | expect(err).to.be.instanceOf(Error); 41 | expect(err).to.have.property('message') 42 | .match(/"notBefore" should be a number of seconds or string representing a timespan/); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | // undefined needs special treatment because {} is not the same as {notBefore: undefined} 49 | it('should error with with value undefined', function (done) { 50 | testUtils.signJWTHelper({}, 'secret', {notBefore: undefined, algorithm: 'HS256'}, (err) => { 51 | testUtils.asyncCheck(done, () => { 52 | expect(err).to.be.instanceOf(Error); 53 | expect(err).to.have.property( 54 | 'message', 55 | '"notBefore" should be a number of seconds or string representing a timespan' 56 | ); 57 | }); 58 | }); 59 | }); 60 | 61 | it('should error when "nbf" is in payload', function (done) { 62 | signWithNotBefore(100, {nbf: 100}, (err) => { 63 | testUtils.asyncCheck(done, () => { 64 | expect(err).to.be.instanceOf(Error); 65 | expect(err).to.have.property( 66 | 'message', 67 | 'Bad "options.notBefore" option the payload already has an "nbf" property.' 68 | ); 69 | }); 70 | }); 71 | }); 72 | 73 | it('should error with a string payload', function (done) { 74 | signWithNotBefore(100, 'a string payload', (err) => { 75 | testUtils.asyncCheck(done, () => { 76 | expect(err).to.be.instanceOf(Error); 77 | expect(err).to.have.property('message', 'invalid notBefore option for string payload'); 78 | }); 79 | }); 80 | }); 81 | 82 | it('should error with a Buffer payload', function (done) { 83 | signWithNotBefore(100, new Buffer('a Buffer payload'), (err) => { 84 | testUtils.asyncCheck(done, () => { 85 | expect(err).to.be.instanceOf(Error); 86 | expect(err).to.have.property('message', 'invalid notBefore option for object payload'); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('`jwt.sign` "nbf" claim validation', function () { 93 | [ 94 | true, 95 | false, 96 | null, 97 | undefined, 98 | '', 99 | ' ', 100 | 'invalid', 101 | [], 102 | ['foo'], 103 | {}, 104 | {foo: 'bar'}, 105 | ].forEach((nbf) => { 106 | it(`should error with with value ${util.inspect(nbf)}`, function (done) { 107 | signWithNotBefore(undefined, {nbf}, (err) => { 108 | testUtils.asyncCheck(done, () => { 109 | expect(err).to.be.instanceOf(Error); 110 | expect(err).to.have.property('message', '"nbf" should be a number of seconds'); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('"nbf" in payload validation', function () { 118 | [ 119 | true, 120 | false, 121 | null, 122 | -Infinity, 123 | Infinity, 124 | NaN, 125 | '', 126 | ' ', 127 | 'invalid', 128 | [], 129 | ['foo'], 130 | {}, 131 | {foo: 'bar'}, 132 | ].forEach((nbf) => { 133 | it(`should error with with value ${util.inspect(nbf)}`, function (done) { 134 | const header = { alg: 'HS256' }; 135 | const payload = { nbf }; 136 | const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); 137 | testUtils.verifyJWTHelper(token, 'secret', {nbf}, (err) => { 138 | testUtils.asyncCheck(done, () => { 139 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 140 | expect(err).to.have.property('message', 'invalid nbf value'); 141 | }); 142 | }); 143 | }); 144 | }) 145 | }); 146 | 147 | describe('when signing and verifying a token with "notBefore" option', function () { 148 | let fakeClock; 149 | beforeEach(function () { 150 | fakeClock = sinon.useFakeTimers({now: 60000}); 151 | }); 152 | 153 | afterEach(function () { 154 | fakeClock.uninstall(); 155 | }); 156 | 157 | it('should set correct "nbf" with negative number of seconds', function (done) { 158 | signWithNotBefore(-10, {}, (e1, token) => { 159 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 160 | testUtils.asyncCheck(done, () => { 161 | expect(e1).to.be.null; 162 | expect(e2).to.be.null; 163 | expect(decoded).to.have.property('nbf', 50); 164 | }); 165 | }) 166 | }); 167 | }); 168 | 169 | it('should set correct "nbf" with positive number of seconds', function (done) { 170 | signWithNotBefore(10, {}, (e1, token) => { 171 | fakeClock.tick(10000); 172 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 173 | testUtils.asyncCheck(done, () => { 174 | expect(e1).to.be.null; 175 | expect(e2).to.be.null; 176 | expect(decoded).to.have.property('nbf', 70); 177 | }); 178 | }) 179 | }); 180 | }); 181 | 182 | it('should set correct "nbf" with zero seconds', function (done) { 183 | signWithNotBefore(0, {}, (e1, token) => { 184 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 185 | testUtils.asyncCheck(done, () => { 186 | expect(e1).to.be.null; 187 | expect(e2).to.be.null; 188 | expect(decoded).to.have.property('nbf', 60); 189 | }); 190 | }) 191 | }); 192 | }); 193 | 194 | it('should set correct "nbf" with negative string timespan', function (done) { 195 | signWithNotBefore('-10 s', {}, (e1, token) => { 196 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 197 | testUtils.asyncCheck(done, () => { 198 | expect(e1).to.be.null; 199 | expect(e2).to.be.null; 200 | expect(decoded).to.have.property('nbf', 50); 201 | }); 202 | }) 203 | }); 204 | }); 205 | 206 | it('should set correct "nbf" with positive string timespan', function (done) { 207 | signWithNotBefore('10 s', {}, (e1, token) => { 208 | fakeClock.tick(10000); 209 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 210 | testUtils.asyncCheck(done, () => { 211 | expect(e1).to.be.null; 212 | expect(e2).to.be.null; 213 | expect(decoded).to.have.property('nbf', 70); 214 | }); 215 | }) 216 | }); 217 | }); 218 | 219 | it('should set correct "nbf" with zero string timespan', function (done) { 220 | signWithNotBefore('0 s', {}, (e1, token) => { 221 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 222 | testUtils.asyncCheck(done, () => { 223 | expect(e1).to.be.null; 224 | expect(e2).to.be.null; 225 | expect(decoded).to.have.property('nbf', 60); 226 | }); 227 | }) 228 | }); 229 | }); 230 | 231 | // TODO an nbf of -Infinity should fail validation 232 | it('should set null "nbf" when given -Infinity', function (done) { 233 | signWithNotBefore(undefined, {nbf: -Infinity}, (err, token) => { 234 | const decoded = jwt.decode(token); 235 | testUtils.asyncCheck(done, () => { 236 | expect(err).to.be.null; 237 | expect(decoded).to.have.property('nbf', null); 238 | }); 239 | }); 240 | }); 241 | 242 | // TODO an nbf of Infinity should fail validation 243 | it('should set null "nbf" when given value Infinity', function (done) { 244 | signWithNotBefore(undefined, {nbf: Infinity}, (err, token) => { 245 | const decoded = jwt.decode(token); 246 | testUtils.asyncCheck(done, () => { 247 | expect(err).to.be.null; 248 | expect(decoded).to.have.property('nbf', null); 249 | }); 250 | }); 251 | }); 252 | 253 | // TODO an nbf of NaN should fail validation 254 | it('should set null "nbf" when given value NaN', function (done) { 255 | signWithNotBefore(undefined, {nbf: NaN}, (err, token) => { 256 | const decoded = jwt.decode(token); 257 | testUtils.asyncCheck(done, () => { 258 | expect(err).to.be.null; 259 | expect(decoded).to.have.property('nbf', null); 260 | }); 261 | }); 262 | }); 263 | 264 | it('should set correct "nbf" when "iat" is passed', function (done) { 265 | signWithNotBefore(-10, {iat: 40}, (e1, token) => { 266 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 267 | testUtils.asyncCheck(done, () => { 268 | expect(e1).to.be.null; 269 | expect(e2).to.be.null; 270 | expect(decoded).to.have.property('nbf', 30); 271 | }); 272 | }) 273 | }); 274 | }); 275 | 276 | it('should verify "nbf" using "clockTimestamp"', function (done) { 277 | signWithNotBefore(10, {}, (e1, token) => { 278 | testUtils.verifyJWTHelper(token, 'secret', {clockTimestamp: 70}, (e2, decoded) => { 279 | testUtils.asyncCheck(done, () => { 280 | expect(e1).to.be.null; 281 | expect(e2).to.be.null; 282 | expect(decoded).to.have.property('iat', 60); 283 | expect(decoded).to.have.property('nbf', 70); 284 | }); 285 | }) 286 | }); 287 | }); 288 | 289 | it('should verify "nbf" using "clockTolerance"', function (done) { 290 | signWithNotBefore(5, {}, (e1, token) => { 291 | testUtils.verifyJWTHelper(token, 'secret', {clockTolerance: 6}, (e2, decoded) => { 292 | testUtils.asyncCheck(done, () => { 293 | expect(e1).to.be.null; 294 | expect(e2).to.be.null; 295 | expect(decoded).to.have.property('iat', 60); 296 | expect(decoded).to.have.property('nbf', 65); 297 | }); 298 | }) 299 | }); 300 | }); 301 | 302 | it('should ignore a not active token when "ignoreNotBefore" is true', function (done) { 303 | signWithNotBefore('10 s', {}, (e1, token) => { 304 | testUtils.verifyJWTHelper(token, 'secret', {ignoreNotBefore: true}, (e2, decoded) => { 305 | testUtils.asyncCheck(done, () => { 306 | expect(e1).to.be.null; 307 | expect(e2).to.be.null; 308 | expect(decoded).to.have.property('iat', 60); 309 | expect(decoded).to.have.property('nbf', 70); 310 | }); 311 | }) 312 | }); 313 | }); 314 | 315 | it('should error on verify if "nbf" is after current time', function (done) { 316 | signWithNotBefore(undefined, {nbf: 61}, (e1, token) => { 317 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2) => { 318 | testUtils.asyncCheck(done, () => { 319 | expect(e1).to.be.null; 320 | expect(e2).to.be.instanceOf(jwt.NotBeforeError); 321 | expect(e2).to.have.property('message', 'jwt not active'); 322 | }); 323 | }) 324 | }); 325 | }); 326 | 327 | it('should error on verify if "nbf" is after current time using clockTolerance', function (done) { 328 | signWithNotBefore(5, {}, (e1, token) => { 329 | testUtils.verifyJWTHelper(token, 'secret', {clockTolerance: 4}, (e2) => { 330 | testUtils.asyncCheck(done, () => { 331 | expect(e1).to.be.null; 332 | expect(e2).to.be.instanceOf(jwt.NotBeforeError); 333 | expect(e2).to.have.property('message', 'jwt not active'); 334 | }); 335 | }) 336 | }); 337 | }); 338 | }); 339 | }); 340 | -------------------------------------------------------------------------------- /test/claim-private.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const util = require('util'); 5 | const testUtils = require('./test-utils'); 6 | 7 | function signWithPayload(payload, callback) { 8 | testUtils.signJWTHelper(payload, 'secret', {algorithm: 'HS256'}, callback); 9 | } 10 | 11 | describe('with a private claim', function() { 12 | [ 13 | true, 14 | false, 15 | null, 16 | -1, 17 | 0, 18 | 1, 19 | -1.1, 20 | 1.1, 21 | '', 22 | 'private claim', 23 | 'UTF8 - José', 24 | [], 25 | ['foo'], 26 | {}, 27 | {foo: 'bar'}, 28 | ].forEach((privateClaim) => { 29 | it(`should sign and verify with claim of ${util.inspect(privateClaim)}`, function (done) { 30 | signWithPayload({privateClaim}, (e1, token) => { 31 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 32 | testUtils.asyncCheck(done, () => { 33 | expect(e1).to.be.null; 34 | expect(e2).to.be.null; 35 | expect(decoded).to.have.property('privateClaim').to.deep.equal(privateClaim); 36 | }); 37 | }) 38 | }); 39 | }); 40 | }); 41 | 42 | // these values JSON.stringify to null 43 | [ 44 | -Infinity, 45 | Infinity, 46 | NaN, 47 | ].forEach((privateClaim) => { 48 | it(`should sign and verify with claim of ${util.inspect(privateClaim)}`, function (done) { 49 | signWithPayload({privateClaim}, (e1, token) => { 50 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 51 | testUtils.asyncCheck(done, () => { 52 | expect(e1).to.be.null; 53 | expect(e2).to.be.null; 54 | expect(decoded).to.have.property('privateClaim', null); 55 | }); 56 | }) 57 | }); 58 | }); 59 | }); 60 | 61 | // private claims with value undefined are not added to the payload 62 | it(`should sign and verify with claim of undefined`, function (done) { 63 | signWithPayload({privateClaim: undefined}, (e1, token) => { 64 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 65 | testUtils.asyncCheck(done, () => { 66 | expect(e1).to.be.null; 67 | expect(e2).to.be.null; 68 | expect(decoded).to.not.have.property('privateClaim'); 69 | }); 70 | }) 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/claim-sub.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const util = require('util'); 6 | const testUtils = require('./test-utils'); 7 | 8 | function signWithSubject(subject, payload, callback) { 9 | const options = {algorithm: 'HS256'}; 10 | if (subject !== undefined) { 11 | options.subject = subject; 12 | } 13 | testUtils.signJWTHelper(payload, 'secret', options, callback); 14 | } 15 | 16 | describe('subject', function() { 17 | describe('`jwt.sign` "subject" option validation', function () { 18 | [ 19 | true, 20 | false, 21 | null, 22 | -1, 23 | 0, 24 | 1, 25 | -1.1, 26 | 1.1, 27 | -Infinity, 28 | Infinity, 29 | NaN, 30 | [], 31 | ['foo'], 32 | {}, 33 | {foo: 'bar'}, 34 | ].forEach((subject) => { 35 | it(`should error with with value ${util.inspect(subject)}`, function (done) { 36 | signWithSubject(subject, {}, (err) => { 37 | testUtils.asyncCheck(done, () => { 38 | expect(err).to.be.instanceOf(Error); 39 | expect(err).to.have.property('message', '"subject" must be a string'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | // undefined needs special treatment because {} is not the same as {subject: undefined} 46 | it('should error with with value undefined', function (done) { 47 | testUtils.signJWTHelper({}, 'secret', {subject: undefined, algorithm: 'HS256'}, (err) => { 48 | testUtils.asyncCheck(done, () => { 49 | expect(err).to.be.instanceOf(Error); 50 | expect(err).to.have.property('message', '"subject" must be a string'); 51 | }); 52 | }); 53 | }); 54 | 55 | it('should error when "sub" is in payload', function (done) { 56 | signWithSubject('foo', {sub: 'bar'}, (err) => { 57 | testUtils.asyncCheck(done, () => { 58 | expect(err).to.be.instanceOf(Error); 59 | expect(err).to.have.property( 60 | 'message', 61 | 'Bad "options.subject" option. The payload already has an "sub" property.' 62 | ); 63 | }); 64 | }); 65 | }); 66 | 67 | it('should error with a string payload', function (done) { 68 | signWithSubject('foo', 'a string payload', (err) => { 69 | testUtils.asyncCheck(done, () => { 70 | expect(err).to.be.instanceOf(Error); 71 | expect(err).to.have.property( 72 | 'message', 73 | 'invalid subject option for string payload' 74 | ); 75 | }); 76 | }); 77 | }); 78 | 79 | it('should error with a Buffer payload', function (done) { 80 | signWithSubject('foo', new Buffer('a Buffer payload'), (err) => { 81 | testUtils.asyncCheck(done, () => { 82 | expect(err).to.be.instanceOf(Error); 83 | expect(err).to.have.property( 84 | 'message', 85 | 'invalid subject option for object payload' 86 | ); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('when signing and verifying a token with "subject" option', function () { 93 | it('should verify with a string "subject"', function (done) { 94 | signWithSubject('foo', {}, (e1, token) => { 95 | testUtils.verifyJWTHelper(token, 'secret', {subject: 'foo'}, (e2, decoded) => { 96 | testUtils.asyncCheck(done, () => { 97 | expect(e1).to.be.null; 98 | expect(e2).to.be.null; 99 | expect(decoded).to.have.property('sub', 'foo'); 100 | }); 101 | }) 102 | }); 103 | }); 104 | 105 | it('should verify with a string "sub"', function (done) { 106 | signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { 107 | testUtils.verifyJWTHelper(token, 'secret', {subject: 'foo'}, (e2, decoded) => { 108 | testUtils.asyncCheck(done, () => { 109 | expect(e1).to.be.null; 110 | expect(e2).to.be.null; 111 | expect(decoded).to.have.property('sub', 'foo'); 112 | }); 113 | }) 114 | }); 115 | }); 116 | 117 | it('should not verify "sub" if verify "subject" option not provided', function(done) { 118 | signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { 119 | testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { 120 | testUtils.asyncCheck(done, () => { 121 | expect(e1).to.be.null; 122 | expect(e2).to.be.null; 123 | expect(decoded).to.have.property('sub', 'foo'); 124 | }); 125 | }) 126 | }); 127 | }); 128 | 129 | it('should error if "sub" does not match verify "subject" option', function(done) { 130 | signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { 131 | testUtils.verifyJWTHelper(token, 'secret', {subject: 'bar'}, (e2) => { 132 | testUtils.asyncCheck(done, () => { 133 | expect(e1).to.be.null; 134 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 135 | expect(e2).to.have.property('message', 'jwt subject invalid. expected: bar'); 136 | }); 137 | }) 138 | }); 139 | }); 140 | 141 | it('should error without "sub" and with verify "subject" option', function(done) { 142 | signWithSubject(undefined, {}, (e1, token) => { 143 | testUtils.verifyJWTHelper(token, 'secret', {subject: 'foo'}, (e2) => { 144 | testUtils.asyncCheck(done, () => { 145 | expect(e1).to.be.null; 146 | expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); 147 | expect(e2).to.have.property('message', 'jwt subject invalid. expected: foo'); 148 | }); 149 | }) 150 | }); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/decoding.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('decoding', function() { 5 | 6 | it('should not crash when decoding a null token', function () { 7 | var decoded = jwt.decode("null"); 8 | expect(decoded).to.equal(null); 9 | }); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /test/dsa-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIGWAIBAAKCAgEArzbPbt//BQpsYsnoZR4R9nXgcuvcXoH8WZjRsb4ZPfVJGchG 3 | 7CfRMlG0HR34vcUpehNj5pAavErhfNnk1CEal0TyDsOkBY/+JG239zXgRzMYjSE6 4 | ptX5kj5pGv0uXVoozSP/JZblI8/Spd6TZkblLNAYOl3ssfcUGN4NFDXlzmiWvP+q 5 | 6ZUgE8tD7CSryicICKmXcVQIa6AG8ultYa6mBAaewzMbiIt2TUo9smglpEqGeHoL 6 | CuLb3e7zLf0AhWDZOgTTfe1KFEiK6TXMe9HWYeP3MPuyKhS20GmT/Zcu5VN4wbr0 7 | bP+mTWk700oLJ0OPQ6YgGkyqBmh/Bsi/TqnpJWS/mjRbJEe3E2NmNMwmP4jwJ79V 8 | JClp5Gg9kbM6hPkmGNnhbbFzn3kwY3pi9/AiqpGyr3GUPhXvP7fYwAu/A5ISKw8r 9 | 87j/EJntyIzm51fcm8Q0mq1IDt4tNkIOwJEIc45h9r7ZC1VAKkzlCa7XT04GguFo 10 | JMaJBYESYcOAmbKRojo8P/cN4fPuemuhQFQplkFIM6FtG9cJMo2ayp6ukH9Up8tn 11 | 8j7YgE/m9BL9SnUIbNlti9j0cNgeKVn24WC38hw9D8M0/sR5gYyclWh/OotCttoQ 12 | I8ySZzSvB4GARZHbexagvg1EdV93ctYyAWGLkpJYAzuiXbt7FayG7e2ifYkCIQDp 13 | IldsAFGVaiJRQdiKsWdReOSjzH6h8cw6Co3OCISiOQKCAgEAnSU29U65jK3W2BiA 14 | fKTlTBx2yDUCDFeqnla5arZ2njGsUKiP2nocArAPLQggwk9rfqufybQltM8+zjmE 15 | zeb4mUCVhSbTH7BvP903U0YEabZJCHLx80nTywq2RgQs0Qmn43vs2U5EidYR0xj8 16 | CCNAH5gdzd9/CL1RYACHAf7zj4n68ZaNkAy9Jz1JjYXjP6IAxJh1W/Y0vsdFdIJ/ 17 | dnuxsyMCUCSwDvSNApSfATO/tw+DCVpGgKo4qE8b8lsfXKeihuMzyXuSe/D98YN2 18 | UFWRTQ6gFxGrntg3LOn41RXSkXxzixgl7quacIJzm8jrFkDJSx4AZ8rgt/9JbThA 19 | XF9PVlCVv7GL1NztUs4cDK+zsJld4O1rlI3QOz5DWq9oA+Hj1MN3L9IW3Iv2Offo 20 | AaubXJhuv0xPWYmtCo06mPgSwkWPjDnGCbp1vuI8zPTsfyhsahuKeW0h8JttW4GB 21 | 6CTtC1AVWA1pJug5pBo36S5G24ihRsdG3Q5/aTlnke7t7H1Tkh2KuvV9hD5a5Xtw 22 | cnuiEcKjyR0FWR81RdsAKh+7QNI3Lx75c95i22Aupon5R/Qkb05VzHdd299bb78c 23 | x5mW8Dsg4tKLF7kpDAcWmx7JpkPHQ+5V9N766sfZ+z/PiVWfNAK8gzJRn/ceLQcK 24 | C6uOhcZgN0o4UYrmYEy9icxJ44wCggIBAIu+yagyVMS+C5OqOprmtteh/+MyaYI+ 25 | Q3oPXFR8eHLJftsBWev1kRfje1fdxzzx/k4SQMRbxxbMtGV74KNwRUzEWOkoyAHP 26 | AAjhMio1mxknPwAxRjWDOSE0drGJPyGpI9ZfpMUtvekQO7MCGqa45vPldY10RwZC 27 | VN66AIpxSF0MG1OEmgD+noHMI7moclw/nw+ZUPaIFxvPstlD4EsPDkdE0I6x3k3b 28 | UXlWAYAJFR6fNf8+Ki3xnjLjW9da3cU/p2H7+LrFDP+kPUGJpqr4bG606GUcV3Cl 29 | dznoqlgaudWgcQCQx0NPzi7k5O7PXr7C3UU0cg+5+GkviIzogaioxidvvchnG+UU 30 | 0y5nVuji6G69j5sUhlcFXte31Nte2VUb6P8umo+mbDT0UkZZZzoOsCpw+cJ8OHOV 31 | emFIhVphNHqQt20Tq6WVRBx+p4+YNWiThvmLtmLh0QghdnUrJZxyXx7/p8K5SE9/ 32 | +qU11t5dUvYS+53U1gJ2kgIFO4Zt6gaoOyexTt5f4Ganh9IcJ01wegl5WT58aDtf 33 | hmw0HnOrgbWt4lRkxOra281hL74xcgtgMZQ32PTOy8wTEVTk03mmqlIq/dV4jgBc 34 | Nh1FGQwGEeGlfbuNSB4nqgMN6zn1PmI7oCWLD9XLR6VZTebF7pGfpHtYczyivuxf 35 | e1YOro6e0mUqAiEAx4K3cPG3dxH91uU3L+sS2vzqXEVn2BmSMmkGczSOgn4= 36 | -----END DSA PRIVATE KEY----- 37 | -------------------------------------------------------------------------------- /test/dsa-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIGSDCCBDoGByqGSM44BAEwggQtAoICAQCvNs9u3/8FCmxiyehlHhH2deBy69xe 3 | gfxZmNGxvhk99UkZyEbsJ9EyUbQdHfi9xSl6E2PmkBq8SuF82eTUIRqXRPIOw6QF 4 | j/4kbbf3NeBHMxiNITqm1fmSPmka/S5dWijNI/8lluUjz9Kl3pNmRuUs0Bg6Xeyx 5 | 9xQY3g0UNeXOaJa8/6rplSATy0PsJKvKJwgIqZdxVAhroAby6W1hrqYEBp7DMxuI 6 | i3ZNSj2yaCWkSoZ4egsK4tvd7vMt/QCFYNk6BNN97UoUSIrpNcx70dZh4/cw+7Iq 7 | FLbQaZP9ly7lU3jBuvRs/6ZNaTvTSgsnQ49DpiAaTKoGaH8GyL9OqeklZL+aNFsk 8 | R7cTY2Y0zCY/iPAnv1UkKWnkaD2RszqE+SYY2eFtsXOfeTBjemL38CKqkbKvcZQ+ 9 | Fe8/t9jAC78DkhIrDyvzuP8Qme3IjObnV9ybxDSarUgO3i02Qg7AkQhzjmH2vtkL 10 | VUAqTOUJrtdPTgaC4WgkxokFgRJhw4CZspGiOjw/9w3h8+56a6FAVCmWQUgzoW0b 11 | 1wkyjZrKnq6Qf1Sny2fyPtiAT+b0Ev1KdQhs2W2L2PRw2B4pWfbhYLfyHD0PwzT+ 12 | xHmBjJyVaH86i0K22hAjzJJnNK8HgYBFkdt7FqC+DUR1X3dy1jIBYYuSklgDO6Jd 13 | u3sVrIbt7aJ9iQIhAOkiV2wAUZVqIlFB2IqxZ1F45KPMfqHxzDoKjc4IhKI5AoIC 14 | AQCdJTb1TrmMrdbYGIB8pOVMHHbINQIMV6qeVrlqtnaeMaxQqI/aehwCsA8tCCDC 15 | T2t+q5/JtCW0zz7OOYTN5viZQJWFJtMfsG8/3TdTRgRptkkIcvHzSdPLCrZGBCzR 16 | Cafje+zZTkSJ1hHTGPwII0AfmB3N338IvVFgAIcB/vOPifrxlo2QDL0nPUmNheM/ 17 | ogDEmHVb9jS+x0V0gn92e7GzIwJQJLAO9I0ClJ8BM7+3D4MJWkaAqjioTxvyWx9c 18 | p6KG4zPJe5J78P3xg3ZQVZFNDqAXEaue2Dcs6fjVFdKRfHOLGCXuq5pwgnObyOsW 19 | QMlLHgBnyuC3/0ltOEBcX09WUJW/sYvU3O1SzhwMr7OwmV3g7WuUjdA7PkNar2gD 20 | 4ePUw3cv0hbci/Y59+gBq5tcmG6/TE9Zia0KjTqY+BLCRY+MOcYJunW+4jzM9Ox/ 21 | KGxqG4p5bSHwm21bgYHoJO0LUBVYDWkm6DmkGjfpLkbbiKFGx0bdDn9pOWeR7u3s 22 | fVOSHYq69X2EPlrle3Bye6IRwqPJHQVZHzVF2wAqH7tA0jcvHvlz3mLbYC6miflH 23 | 9CRvTlXMd13b31tvvxzHmZbwOyDi0osXuSkMBxabHsmmQ8dD7lX03vrqx9n7P8+J 24 | VZ80AryDMlGf9x4tBwoLq46FxmA3SjhRiuZgTL2JzEnjjAOCAgYAAoICAQCLvsmo 25 | MlTEvguTqjqa5rbXof/jMmmCPkN6D1xUfHhyyX7bAVnr9ZEX43tX3cc88f5OEkDE 26 | W8cWzLRle+CjcEVMxFjpKMgBzwAI4TIqNZsZJz8AMUY1gzkhNHaxiT8hqSPWX6TF 27 | Lb3pEDuzAhqmuObz5XWNdEcGQlTeugCKcUhdDBtThJoA/p6BzCO5qHJcP58PmVD2 28 | iBcbz7LZQ+BLDw5HRNCOsd5N21F5VgGACRUenzX/Piot8Z4y41vXWt3FP6dh+/i6 29 | xQz/pD1Biaaq+GxutOhlHFdwpXc56KpYGrnVoHEAkMdDT84u5OTuz16+wt1FNHIP 30 | ufhpL4iM6IGoqMYnb73IZxvlFNMuZ1bo4uhuvY+bFIZXBV7Xt9TbXtlVG+j/LpqP 31 | pmw09FJGWWc6DrAqcPnCfDhzlXphSIVaYTR6kLdtE6ullUQcfqePmDVok4b5i7Zi 32 | 4dEIIXZ1KyWccl8e/6fCuUhPf/qlNdbeXVL2Evud1NYCdpICBTuGbeoGqDsnsU7e 33 | X+Bmp4fSHCdNcHoJeVk+fGg7X4ZsNB5zq4G1reJUZMTq2tvNYS++MXILYDGUN9j0 34 | zsvMExFU5NN5pqpSKv3VeI4AXDYdRRkMBhHhpX27jUgeJ6oDDes59T5iO6Aliw/V 35 | y0elWU3mxe6Rn6R7WHM8or7sX3tWDq6OntJlKg== 36 | -----END PUBLIC KEY----- 37 | -------------------------------------------------------------------------------- /test/ecdsa-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////////// 3 | /////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6 4 | k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+ 5 | kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK 6 | fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz 7 | ucrC/GMlUQIBAQ== 8 | -----END EC PARAMETERS----- 9 | -----BEGIN EC PRIVATE KEY----- 10 | MIIBaAIBAQQgeg2m9tJJsnURyjTUihohiJahj9ETy3csUIt4EYrV+J2ggfowgfcC 11 | AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// 12 | MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr 13 | vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE 14 | axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W 15 | K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 16 | YyVRAgEBoUQDQgAEEWluurrkZECnq27UpNauq16f9+5DDMFJZ3HV43Ujc3tcXQ++ 17 | N1T/0CAA8ve286f32s7rkqX/pPokI/HBpP5p3g== 18 | -----END EC PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/ecdsa-public-invalid.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA 3 | AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// 4 | ///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd 5 | NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 6 | RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA 7 | //////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABEfZiYJDbghTGQ+KGnHGSl6K 8 | yUqK/BL2uJIg7Z0bx48v6+L7Ve8MCS17eptkMT2e4l5B/ZGDVUHb6uZ5xFROLBw= 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/ecdsa-public-x509.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDGjCCAsKgAwIBAgIJANuPNBWwp6wzMAkGByqGSM49BAEwRTELMAkGA1UEBhMC 3 | QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp 4 | dHMgUHR5IEx0ZDAeFw0xNzA2MTAxMTAzMjJaFw0yNzA2MDgxMTAzMjJaMEUxCzAJ 5 | BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l 6 | dCBXaWRnaXRzIFB0eSBMdGQwggFLMIIBAwYHKoZIzj0CATCB9wIBATAsBgcqhkjO 7 | PQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg/////wAA 8 | AAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaYhrxlHQaw 9 | zFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy4SxCR/i8 10 | 5uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2 11 | QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQEDQgAE 12 | EWluurrkZECnq27UpNauq16f9+5DDMFJZ3HV43Ujc3tcXQ++N1T/0CAA8ve286f3 13 | 2s7rkqX/pPokI/HBpP5p3qOBpzCBpDAdBgNVHQ4EFgQUAF43lnAvCztZZGaGMoxs 14 | cp6tpz8wdQYDVR0jBG4wbIAUAF43lnAvCztZZGaGMoxscp6tpz+hSaRHMEUxCzAJ 15 | BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l 16 | dCBXaWRnaXRzIFB0eSBMdGSCCQDbjzQVsKesMzAMBgNVHRMEBTADAQH/MAkGByqG 17 | SM49BAEDRwAwRAIgV039oh2RtcSwywQ/0dWAwc20NHxrgmKoQ5A3AS5A9d0CIBCV 18 | 2AlKDFjmDC7zjldNhWbMcIlSSj71ghhhxeS0F8v1 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/ecdsa-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA 3 | AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// 4 | ///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd 5 | NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 6 | RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA 7 | //////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABBFpbrq65GRAp6tu1KTWrqte 8 | n/fuQwzBSWdx1eN1I3N7XF0PvjdU/9AgAPL3tvOn99rO65Kl/6T6JCPxwaT+ad4= 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/encoding.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | var atob = require('atob'); 4 | 5 | describe('encoding', function() { 6 | 7 | function b64_to_utf8 (str) { 8 | return decodeURIComponent(escape(atob( str ))); 9 | } 10 | 11 | it('should properly encode the token (utf8)', function () { 12 | var expected = 'José'; 13 | var token = jwt.sign({ name: expected }, 'shhhhh'); 14 | var decoded_name = JSON.parse(b64_to_utf8(token.split('.')[1])).name; 15 | expect(decoded_name).to.equal(expected); 16 | }); 17 | 18 | it('should properly encode the token (binary)', function () { 19 | var expected = 'José'; 20 | var token = jwt.sign({ name: expected }, 'shhhhh', { encoding: 'binary' }); 21 | var decoded_name = JSON.parse(atob(token.split('.')[1])).name; 22 | expect(decoded_name).to.equal(expected); 23 | }); 24 | 25 | it('should return the same result when decoding', function () { 26 | var username = '測試'; 27 | 28 | var token = jwt.sign({ 29 | username: username 30 | }, 'test'); 31 | 32 | var payload = jwt.verify(token, 'test'); 33 | 34 | expect(payload.username).to.equal(username); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /test/expires_format.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('expires option', function() { 5 | 6 | it('should throw on deprecated expiresInSeconds option', function () { 7 | expect(function () { 8 | jwt.sign({foo: 123}, '123', { expiresInSeconds: 5 }); 9 | }).to.throw('"expiresInSeconds" is not allowed'); 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/header-kid.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const util = require('util'); 6 | const testUtils = require('./test-utils'); 7 | 8 | function signWithKeyId(keyid, payload, callback) { 9 | const options = {algorithm: 'HS256'}; 10 | if (keyid !== undefined) { 11 | options.keyid = keyid; 12 | } 13 | testUtils.signJWTHelper(payload, 'secret', options, callback); 14 | } 15 | 16 | describe('keyid', function() { 17 | describe('`jwt.sign` "keyid" option validation', function () { 18 | [ 19 | true, 20 | false, 21 | null, 22 | -1, 23 | 0, 24 | 1, 25 | -1.1, 26 | 1.1, 27 | -Infinity, 28 | Infinity, 29 | NaN, 30 | [], 31 | ['foo'], 32 | {}, 33 | {foo: 'bar'}, 34 | ].forEach((keyid) => { 35 | it(`should error with with value ${util.inspect(keyid)}`, function (done) { 36 | signWithKeyId(keyid, {}, (err) => { 37 | testUtils.asyncCheck(done, () => { 38 | expect(err).to.be.instanceOf(Error); 39 | expect(err).to.have.property('message', '"keyid" must be a string'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | // undefined needs special treatment because {} is not the same as {keyid: undefined} 46 | it('should error with with value undefined', function (done) { 47 | testUtils.signJWTHelper({}, 'secret', {keyid: undefined, algorithm: 'HS256'}, (err) => { 48 | testUtils.asyncCheck(done, () => { 49 | expect(err).to.be.instanceOf(Error); 50 | expect(err).to.have.property('message', '"keyid" must be a string'); 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('when signing a token', function () { 57 | it('should not add "kid" header when "keyid" option not provided', function(done) { 58 | signWithKeyId(undefined, {}, (err, token) => { 59 | testUtils.asyncCheck(done, () => { 60 | const decoded = jwt.decode(token, {complete: true}); 61 | expect(err).to.be.null; 62 | expect(decoded.header).to.not.have.property('kid'); 63 | }); 64 | }); 65 | }); 66 | 67 | it('should add "kid" header when "keyid" option is provided and an object payload', function(done) { 68 | signWithKeyId('foo', {}, (err, token) => { 69 | testUtils.asyncCheck(done, () => { 70 | const decoded = jwt.decode(token, {complete: true}); 71 | expect(err).to.be.null; 72 | expect(decoded.header).to.have.property('kid', 'foo'); 73 | }); 74 | }); 75 | }); 76 | 77 | it('should add "kid" header when "keyid" option is provided and a Buffer payload', function(done) { 78 | signWithKeyId('foo', new Buffer('a Buffer payload'), (err, token) => { 79 | testUtils.asyncCheck(done, () => { 80 | const decoded = jwt.decode(token, {complete: true}); 81 | expect(err).to.be.null; 82 | expect(decoded.header).to.have.property('kid', 'foo'); 83 | }); 84 | }); 85 | }); 86 | 87 | it('should add "kid" header when "keyid" option is provided and a string payload', function(done) { 88 | signWithKeyId('foo', 'a string payload', (err, token) => { 89 | testUtils.asyncCheck(done, () => { 90 | const decoded = jwt.decode(token, {complete: true}); 91 | expect(err).to.be.null; 92 | expect(decoded.header).to.have.property('kid', 'foo'); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/invalid_exp.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('invalid expiration', function() { 5 | 6 | it('should fail with string', function (done) { 7 | var broken_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxMjMiLCJmb28iOiJhZGFzIn0.cDa81le-pnwJMcJi3o3PBwB7cTJMiXCkizIhxbXAKRg'; 8 | 9 | jwt.verify(broken_token, '123', function (err) { 10 | expect(err.name).to.equal('JsonWebTokenError'); 11 | done(); 12 | }); 13 | 14 | }); 15 | 16 | it('should fail with 0', function (done) { 17 | var broken_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjAsImZvbyI6ImFkYXMifQ.UKxix5T79WwfqAA0fLZr6UrhU-jMES2unwCOFa4grEA'; 18 | 19 | jwt.verify(broken_token, '123', function (err) { 20 | expect(err.name).to.equal('TokenExpiredError'); 21 | done(); 22 | }); 23 | 24 | }); 25 | 26 | it('should fail with false', function (done) { 27 | var broken_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOmZhbHNlLCJmb28iOiJhZGFzIn0.iBn33Plwhp-ZFXqppCd8YtED77dwWU0h68QS_nEQL8I'; 28 | 29 | jwt.verify(broken_token, '123', function (err) { 30 | expect(err.name).to.equal('JsonWebTokenError'); 31 | done(); 32 | }); 33 | 34 | }); 35 | 36 | it('should fail with true', function (done) { 37 | var broken_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOnRydWUsImZvbyI6ImFkYXMifQ.eOWfZCTM5CNYHAKSdFzzk2tDkPQmRT17yqllO-ItIMM'; 38 | 39 | jwt.verify(broken_token, '123', function (err) { 40 | expect(err.name).to.equal('JsonWebTokenError'); 41 | done(); 42 | }); 43 | 44 | }); 45 | 46 | it('should fail with object', function (done) { 47 | var broken_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOnt9LCJmb28iOiJhZGFzIn0.1JjCTsWLJ2DF-CfESjLdLfKutUt3Ji9cC7ESlcoBHSY'; 48 | 49 | jwt.verify(broken_token, '123', function (err) { 50 | expect(err.name).to.equal('JsonWebTokenError'); 51 | done(); 52 | }); 53 | 54 | }); 55 | 56 | 57 | }); -------------------------------------------------------------------------------- /test/invalid_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJjCCAg6gAwIBAgIJAMyz3mSPlaW4MA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV 3 | BAMUCyouYXV0aDAuY29tMB4XDTEzMDQxODE3MDE1MFoXDTI2MTIyNjE3MDE1MFow 4 | FjEUMBIGA1UEAxQLKi5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQDZq1Ua0/BGm+TaBFoftKWeYMWrQG9Fx3g7ikErxljmyOvlwqkiat3q 6 | ixX+Dxw9TFb5gbBjNJ+L3nt4YefJgLsYvsHqkOUxWsB+HM/ulJRVnVrZm1tI3Nbg 7 | xO1BQ7DrGfBpq2KCxtQCaQFRlQJw1+qS5LwrdIvihB7Kc142VElCFFHJ6+09eMUy 8 | jy00Z5pfQr4Am6W6eEOS9ObDbNs4XgKOcWe5khWXj3UStou+VgbAg40XcYht2IbY 9 | gMfKF+VUZOy3+e+aRTqPOBU3MAeb0tvCCPUQJbNAUHgSKVhAvNf8mRwttVsOLT70 10 | anjjeCOd7RKS8fVKBwc2KtgNkghYdPY9AgMBAAGjdzB1MB0GA1UdDgQWBBSi4+X0 11 | +MvCKDdd375mDhx/ZBbJ4DBGBgNVHSMEPzA9gBSi4+X0+MvCKDdd375mDhx/ZBbJ 12 | 4KEapBgwFjEUMBIGA1UEAxQLKi5hdXRoMC5jb22CCQDMs95kj5WluDAMBgNVHRME 13 | BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBi0qPe0DzlPSufq+Gdk2Fwf1pGEtjA 14 | D34IxxJ9SX6r1DS/NIP7IOLUnNU8cP8BQWl7i413v29jJsNV457pjdmqf8J7OE9O 15 | eF5Yz1x91gY/27561Iga/TQeIVOlFQAgx66eLfUFFoAig3hz2srZo5TzYBixMJsS 16 | fYMXHPiU7KoLUqYXvpSXIllstQCu51KCC6t9H7wZ92lTES1v76hFY4edQ30sftPo 17 | kjAYWGEhMjPo/r4THcdSMqKXoRtCGEun4pTXid7MJcTgdGDrAJddLWi6SxKecEVB 18 | MhMu4XfUCdxCwqQPjHeJ+zE49A1CUdBB2FN3BNLbmTTwEBgmuwyGRzhj 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/issue_147.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('issue 147 - signing with a sealed payload', function() { 5 | 6 | it('should put the expiration claim', function () { 7 | var token = jwt.sign(Object.seal({foo: 123}), '123', { expiresIn: 10 }); 8 | var result = jwt.verify(token, '123'); 9 | expect(result.exp).to.be.closeTo(Math.floor(Date.now() / 1000) + 10, 0.2); 10 | }); 11 | 12 | }); -------------------------------------------------------------------------------- /test/issue_304.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('issue 304 - verifying values other than strings', function() { 5 | 6 | it('should fail with numbers', function (done) { 7 | jwt.verify(123, 'foo', function (err) { 8 | expect(err.name).to.equal('JsonWebTokenError'); 9 | done(); 10 | }); 11 | }); 12 | 13 | it('should fail with objects', function (done) { 14 | jwt.verify({ foo: 'bar' }, 'biz', function (err) { 15 | expect(err.name).to.equal('JsonWebTokenError'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should fail with arrays', function (done) { 21 | jwt.verify(['foo'], 'bar', function (err) { 22 | expect(err.name).to.equal('JsonWebTokenError'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should fail with functions', function (done) { 28 | jwt.verify(function() {}, 'foo', function (err) { 29 | expect(err.name).to.equal('JsonWebTokenError'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should fail with booleans', function (done) { 35 | jwt.verify(true, 'foo', function (err) { 36 | expect(err.name).to.equal('JsonWebTokenError'); 37 | done(); 38 | }); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/issue_70.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../'); 2 | 3 | describe('issue 70 - public key start with BEING PUBLIC KEY', function () { 4 | 5 | it('should work', function (done) { 6 | var fs = require('fs'); 7 | var cert_pub = fs.readFileSync(__dirname + '/rsa-public.pem'); 8 | var cert_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); 9 | 10 | var token = jwt.sign({ foo: 'bar' }, cert_priv, { algorithm: 'RS256'}); 11 | 12 | jwt.verify(token, cert_pub, done); 13 | }); 14 | 15 | }); -------------------------------------------------------------------------------- /test/jwt.asymmetric_signing.tests.js: -------------------------------------------------------------------------------- 1 | const jwt = require('../index'); 2 | const PS_SUPPORTED = require('../lib/psSupported'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const expect = require('chai').expect; 7 | const assert = require('chai').assert; 8 | const ms = require('ms'); 9 | 10 | function loadKey(filename) { 11 | return fs.readFileSync(path.join(__dirname, filename)); 12 | } 13 | 14 | const algorithms = { 15 | RS256: { 16 | pub_key: loadKey('pub.pem'), 17 | priv_key: loadKey('priv.pem'), 18 | invalid_pub_key: loadKey('invalid_pub.pem') 19 | }, 20 | ES256: { 21 | // openssl ecparam -name secp256r1 -genkey -param_enc explicit -out ecdsa-private.pem 22 | priv_key: loadKey('ecdsa-private.pem'), 23 | // openssl ec -in ecdsa-private.pem -pubout -out ecdsa-public.pem 24 | pub_key: loadKey('ecdsa-public.pem'), 25 | invalid_pub_key: loadKey('ecdsa-public-invalid.pem') 26 | } 27 | }; 28 | 29 | if (PS_SUPPORTED) { 30 | algorithms.PS256 = { 31 | pub_key: loadKey('pub.pem'), 32 | priv_key: loadKey('priv.pem'), 33 | invalid_pub_key: loadKey('invalid_pub.pem') 34 | }; 35 | } 36 | 37 | 38 | describe('Asymmetric Algorithms', function() { 39 | Object.keys(algorithms).forEach(function (algorithm) { 40 | describe(algorithm, function () { 41 | const pub = algorithms[algorithm].pub_key; 42 | const priv = algorithms[algorithm].priv_key; 43 | 44 | // "invalid" means it is not the public key for the loaded "priv" key 45 | const invalid_pub = algorithms[algorithm].invalid_pub_key; 46 | 47 | describe('when signing a token', function () { 48 | const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); 49 | 50 | it('should be syntactically valid', function () { 51 | expect(token).to.be.a('string'); 52 | expect(token.split('.')).to.have.length(3); 53 | }); 54 | 55 | context('asynchronous', function () { 56 | it('should validate with public key', function (done) { 57 | jwt.verify(token, pub, function (err, decoded) { 58 | assert.ok(decoded.foo); 59 | assert.equal('bar', decoded.foo); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should throw with invalid public key', function (done) { 65 | jwt.verify(token, invalid_pub, function (err, decoded) { 66 | assert.isUndefined(decoded); 67 | assert.isNotNull(err); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | context('synchronous', function () { 74 | it('should validate with public key', function () { 75 | const decoded = jwt.verify(token, pub); 76 | assert.ok(decoded.foo); 77 | assert.equal('bar', decoded.foo); 78 | }); 79 | 80 | it('should throw with invalid public key', function () { 81 | const jwtVerify = jwt.verify.bind(null, token, invalid_pub) 82 | assert.throw(jwtVerify, 'invalid signature'); 83 | }); 84 | }); 85 | 86 | }); 87 | 88 | describe('when signing a token with expiration', function () { 89 | it('should be valid expiration', function (done) { 90 | const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: '10m' }); 91 | jwt.verify(token, pub, function (err, decoded) { 92 | assert.isNotNull(decoded); 93 | assert.isNull(err); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should be invalid', function (done) { 99 | // expired token 100 | const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); 101 | jwt.verify(token, pub, function (err, decoded) { 102 | assert.isUndefined(decoded); 103 | assert.isNotNull(err); 104 | assert.equal(err.name, 'TokenExpiredError'); 105 | assert.instanceOf(err.expiredAt, Date); 106 | assert.instanceOf(err, jwt.TokenExpiredError); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('should NOT be invalid', function (done) { 112 | // expired token 113 | const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); 114 | 115 | jwt.verify(token, pub, { ignoreExpiration: true }, function (err, decoded) { 116 | assert.ok(decoded.foo); 117 | assert.equal('bar', decoded.foo); 118 | done(); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('when verifying a malformed token', function () { 124 | it('should throw', function (done) { 125 | jwt.verify('fruit.fruit.fruit', pub, function (err, decoded) { 126 | assert.isUndefined(decoded); 127 | assert.isNotNull(err); 128 | assert.equal(err.name, 'JsonWebTokenError'); 129 | done(); 130 | }); 131 | }); 132 | }); 133 | 134 | describe('when decoding a jwt token with additional parts', function () { 135 | const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); 136 | 137 | it('should throw', function (done) { 138 | jwt.verify(token + '.foo', pub, function (err, decoded) { 139 | assert.isUndefined(decoded); 140 | assert.isNotNull(err); 141 | done(); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('when decoding a invalid jwt token', function () { 147 | it('should return null', function (done) { 148 | const payload = jwt.decode('whatever.token'); 149 | assert.isNull(payload); 150 | done(); 151 | }); 152 | }); 153 | 154 | describe('when decoding a valid jwt token', function () { 155 | it('should return the payload', function (done) { 156 | const obj = { foo: 'bar' }; 157 | const token = jwt.sign(obj, priv, { algorithm: algorithm }); 158 | const payload = jwt.decode(token); 159 | assert.equal(payload.foo, obj.foo); 160 | done(); 161 | }); 162 | it('should return the header and payload and signature if complete option is set', function (done) { 163 | const obj = { foo: 'bar' }; 164 | const token = jwt.sign(obj, priv, { algorithm: algorithm }); 165 | const decoded = jwt.decode(token, { complete: true }); 166 | assert.equal(decoded.payload.foo, obj.foo); 167 | assert.deepEqual(decoded.header, { typ: 'JWT', alg: algorithm }); 168 | assert.ok(typeof decoded.signature == 'string'); 169 | done(); 170 | }); 171 | }); 172 | }); 173 | }); 174 | 175 | describe('when signing a token with an unsupported private key type', function () { 176 | it('should throw an error', function() { 177 | const obj = { foo: 'bar' }; 178 | const key = loadKey('dsa-private.pem'); 179 | const algorithm = 'RS256'; 180 | 181 | expect(function() { 182 | jwt.sign(obj, key, { algorithm }); 183 | }).to.throw('Unknown key type "dsa".'); 184 | }); 185 | }); 186 | 187 | describe('when signing a token with an incorrect private key type', function () { 188 | it('should throw a validation error if key validation is enabled', function() { 189 | const obj = { foo: 'bar' }; 190 | const key = loadKey('rsa-private.pem'); 191 | const algorithm = 'ES256'; 192 | 193 | expect(function() { 194 | jwt.sign(obj, key, { algorithm }); 195 | }).to.throw(/"alg" parameter for "rsa" key type must be one of:/); 196 | }); 197 | 198 | it('should throw an unknown error if key validation is disabled', function() { 199 | const obj = { foo: 'bar' }; 200 | const key = loadKey('rsa-private.pem'); 201 | const algorithm = 'ES256'; 202 | 203 | expect(function() { 204 | jwt.sign(obj, key, { algorithm, allowInvalidAsymmetricKeyTypes: true }); 205 | }).to.not.throw(/"alg" parameter for "rsa" key type must be one of:/); 206 | }); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /test/jwt.hs.tests.js: -------------------------------------------------------------------------------- 1 | const jwt = require('../index'); 2 | 3 | const jws = require('jws'); 4 | const expect = require('chai').expect; 5 | const assert = require('chai').assert; 6 | const { generateKeyPairSync } = require('crypto') 7 | 8 | describe('HS256', function() { 9 | 10 | describe("when signing using HS256", function () { 11 | it('should throw if the secret is an asymmetric key', function () { 12 | const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); 13 | 14 | expect(function () { 15 | jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'HS256' }) 16 | }).to.throw(Error, 'must be a symmetric key') 17 | }) 18 | 19 | it('should throw if the payload is undefined', function () { 20 | expect(function () { 21 | jwt.sign(undefined, "secret", { algorithm: 'HS256' }) 22 | }).to.throw(Error, 'payload is required') 23 | }) 24 | 25 | it('should throw if options is not a plain object', function () { 26 | expect(function () { 27 | jwt.sign({ foo: 'bar' }, "secret", ['HS256']) 28 | }).to.throw(Error, 'Expected "options" to be a plain object') 29 | }) 30 | }) 31 | 32 | describe('with a token signed using HS256', function() { 33 | var secret = 'shhhhhh'; 34 | 35 | var token = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }); 36 | 37 | it('should be syntactically valid', function() { 38 | expect(token).to.be.a('string'); 39 | expect(token.split('.')).to.have.length(3); 40 | }); 41 | 42 | it('should be able to validate without options', function(done) { 43 | var callback = function(err, decoded) { 44 | assert.ok(decoded.foo); 45 | assert.equal('bar', decoded.foo); 46 | done(); 47 | }; 48 | callback.issuer = "shouldn't affect"; 49 | jwt.verify(token, secret, callback ); 50 | }); 51 | 52 | it('should validate with secret', function(done) { 53 | jwt.verify(token, secret, function(err, decoded) { 54 | assert.ok(decoded.foo); 55 | assert.equal('bar', decoded.foo); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should throw with invalid secret', function(done) { 61 | jwt.verify(token, 'invalid secret', function(err, decoded) { 62 | assert.isUndefined(decoded); 63 | assert.isNotNull(err); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should throw with secret and token not signed', function(done) { 69 | const header = { alg: 'none' }; 70 | const payload = { foo: 'bar' }; 71 | const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); 72 | jwt.verify(token, 'secret', function(err, decoded) { 73 | assert.isUndefined(decoded); 74 | assert.isNotNull(err); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('should throw with falsy secret and token not signed', function(done) { 80 | const header = { alg: 'none' }; 81 | const payload = { foo: 'bar' }; 82 | const token = jws.sign({ header, payload, secret: null, encoding: 'utf8' }); 83 | jwt.verify(token, 'secret', function(err, decoded) { 84 | assert.isUndefined(decoded); 85 | assert.isNotNull(err); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('should throw when verifying null', function(done) { 91 | jwt.verify(null, 'secret', function(err, decoded) { 92 | assert.isUndefined(decoded); 93 | assert.isNotNull(err); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should return an error when the token is expired', function(done) { 99 | var token = jwt.sign({ exp: 1 }, secret, { algorithm: 'HS256' }); 100 | jwt.verify(token, secret, { algorithm: 'HS256' }, function(err, decoded) { 101 | assert.isUndefined(decoded); 102 | assert.isNotNull(err); 103 | done(); 104 | }); 105 | }); 106 | 107 | it('should NOT return an error when the token is expired with "ignoreExpiration"', function(done) { 108 | var token = jwt.sign({ exp: 1, foo: 'bar' }, secret, { algorithm: 'HS256' }); 109 | jwt.verify(token, secret, { algorithm: 'HS256', ignoreExpiration: true }, function(err, decoded) { 110 | assert.ok(decoded.foo); 111 | assert.equal('bar', decoded.foo); 112 | assert.isNull(err); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('should default to HS256 algorithm when no options are passed', function() { 118 | var token = jwt.sign({ foo: 'bar' }, secret); 119 | var verifiedToken = jwt.verify(token, secret); 120 | assert.ok(verifiedToken.foo); 121 | assert.equal('bar', verifiedToken.foo); 122 | }); 123 | }); 124 | 125 | describe('should fail verification gracefully with trailing space in the jwt', function() { 126 | var secret = 'shhhhhh'; 127 | var token = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }); 128 | 129 | it('should return the "invalid token" error', function(done) { 130 | var malformedToken = token + ' '; // corrupt the token by adding a space 131 | jwt.verify(malformedToken, secret, { algorithm: 'HS256', ignoreExpiration: true }, function(err) { 132 | assert.isNotNull(err); 133 | assert.equal('JsonWebTokenError', err.name); 134 | assert.equal('invalid token', err.message); 135 | done(); 136 | }); 137 | }); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /test/jwt.malicious.tests.js: -------------------------------------------------------------------------------- 1 | const jwt = require('../index'); 2 | const crypto = require("crypto"); 3 | const {expect} = require('chai'); 4 | const JsonWebTokenError = require("../lib/JsonWebTokenError"); 5 | 6 | describe('when verifying a malicious token', function () { 7 | // attacker has access to the public rsa key, but crafts the token as HS256 8 | // with kid set to the id of the rsa key, instead of the id of the hmac secret. 9 | // const maliciousToken = jwt.sign( 10 | // {foo: 'bar'}, 11 | // pubRsaKey, 12 | // {algorithm: 'HS256', keyid: 'rsaKeyId'} 13 | // ); 14 | // consumer accepts self signed tokens (HS256) and third party tokens (RS256) 15 | const options = {algorithms: ['RS256', 'HS256']}; 16 | 17 | const { 18 | publicKey: pubRsaKey 19 | } = crypto.generateKeyPairSync('rsa', {modulusLength: 2048}); 20 | 21 | it('should not allow HMAC verification with an RSA key in KeyObject format', function () { 22 | const maliciousToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYUtleUlkIn0.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTk1MTA2MDh9.cOcHI1TXPbxTMlyVTfjArSWskrmezbrG8iR7uJHwtrQ'; 23 | 24 | expect(() => jwt.verify(maliciousToken, pubRsaKey, options)).to.throw(JsonWebTokenError, 'must be a symmetric key'); 25 | }) 26 | 27 | it('should not allow HMAC verification with an RSA key in PEM format', function () { 28 | const maliciousToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYUtleUlkIn0.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTk1MTA2MDh9.cOcHI1TXPbxTMlyVTfjArSWskrmezbrG8iR7uJHwtrQ'; 29 | 30 | expect(() => jwt.verify(maliciousToken, pubRsaKey.export({type: 'spki', format: 'pem'}), options)).to.throw(JsonWebTokenError, 'must be a symmetric key'); 31 | }) 32 | 33 | it('should not allow arbitrary execution from malicious Buffers containing objects with overridden toString functions', function () { 34 | const token = jwt.sign({"foo": "bar"}, 'secret') 35 | const maliciousBuffer = {toString: () => {throw new Error("Arbitrary Code Execution")}} 36 | 37 | expect(() => jwt.verify(token, maliciousBuffer)).to.throw(Error, 'not valid key material'); 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/noTimestamp.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('noTimestamp', function() { 5 | 6 | it('should work with string', function () { 7 | var token = jwt.sign({foo: 123}, '123', { expiresIn: '5m' , noTimestamp: true }); 8 | var result = jwt.verify(token, '123'); 9 | expect(result.exp).to.be.closeTo(Math.floor(Date.now() / 1000) + (5*60), 0.5); 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/non_object_values.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('non_object_values values', function() { 5 | 6 | it('should work with string', function () { 7 | var token = jwt.sign('hello', '123'); 8 | var result = jwt.verify(token, '123'); 9 | expect(result).to.equal('hello'); 10 | }); 11 | 12 | it('should work with number', function () { 13 | var token = jwt.sign(123, '123'); 14 | var result = jwt.verify(token, '123'); 15 | expect(result).to.equal('123'); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/option-complete.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jws = require('jws'); 4 | const expect = require('chai').expect; 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const testUtils = require('./test-utils') 8 | 9 | describe('complete option', function () { 10 | const secret = fs.readFileSync(path.join(__dirname, 'priv.pem')); 11 | const pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); 12 | 13 | const header = { alg: 'RS256' }; 14 | const payload = { iat: Math.floor(Date.now() / 1000 ) }; 15 | const signed = jws.sign({ header, payload, secret, encoding: 'utf8' }); 16 | const signature = jws.decode(signed).signature; 17 | 18 | [ 19 | { 20 | description: 'should return header, payload and signature', 21 | complete: true, 22 | }, 23 | ].forEach((testCase) => { 24 | it(testCase.description, function (done) { 25 | testUtils.verifyJWTHelper(signed, pub, { typ: 'JWT', complete: testCase.complete }, (err, decoded) => { 26 | testUtils.asyncCheck(done, () => { 27 | expect(err).to.be.null; 28 | expect(decoded.header).to.have.property('alg', header.alg); 29 | expect(decoded.payload).to.have.property('iat', payload.iat); 30 | expect(decoded).to.have.property('signature', signature); 31 | }); 32 | }); 33 | }); 34 | }); 35 | [ 36 | { 37 | description: 'should return payload', 38 | complete: false, 39 | }, 40 | ].forEach((testCase) => { 41 | it(testCase.description, function (done) { 42 | testUtils.verifyJWTHelper(signed, pub, { typ: 'JWT', complete: testCase.complete }, (err, decoded) => { 43 | testUtils.asyncCheck(done, () => { 44 | expect(err).to.be.null; 45 | expect(decoded.header).to.be.undefined; 46 | expect(decoded.payload).to.be.undefined; 47 | expect(decoded.signature).to.be.undefined; 48 | expect(decoded).to.have.property('iat', payload.iat); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/option-maxAge.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | const util = require('util'); 7 | 8 | describe('maxAge option', function() { 9 | let token; 10 | 11 | let fakeClock; 12 | beforeEach(function() { 13 | fakeClock = sinon.useFakeTimers({now: 60000}); 14 | token = jwt.sign({iat: 70}, 'secret', {algorithm: 'HS256'}); 15 | }); 16 | 17 | afterEach(function() { 18 | fakeClock.uninstall(); 19 | }); 20 | 21 | [ 22 | { 23 | description: 'should work with a positive string value', 24 | maxAge: '3s', 25 | }, 26 | { 27 | description: 'should work with a negative string value', 28 | maxAge: '-3s', 29 | }, 30 | { 31 | description: 'should work with a positive numeric value', 32 | maxAge: 3, 33 | }, 34 | { 35 | description: 'should work with a negative numeric value', 36 | maxAge: -3, 37 | }, 38 | ].forEach((testCase) => { 39 | it(testCase.description, function (done) { 40 | expect(jwt.verify(token, 'secret', {maxAge: '3s', algorithm: 'HS256'})).to.not.throw; 41 | jwt.verify(token, 'secret', {maxAge: testCase.maxAge, algorithm: 'HS256'}, (err) => { 42 | expect(err).to.be.null; 43 | done(); 44 | }) 45 | }); 46 | }); 47 | 48 | [ 49 | true, 50 | 'invalid', 51 | [], 52 | ['foo'], 53 | {}, 54 | {foo: 'bar'}, 55 | ].forEach((maxAge) => { 56 | it(`should error with value ${util.inspect(maxAge)}`, function (done) { 57 | expect(() => jwt.verify(token, 'secret', {maxAge, algorithm: 'HS256'})).to.throw( 58 | jwt.JsonWebTokenError, 59 | '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' 60 | ); 61 | jwt.verify(token, 'secret', {maxAge, algorithm: 'HS256'}, (err) => { 62 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 63 | expect(err.message).to.equal( 64 | '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' 65 | ); 66 | done(); 67 | }) 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/option-nonce.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const util = require('util'); 6 | const testUtils = require('./test-utils') 7 | 8 | describe('nonce option', function () { 9 | let token; 10 | 11 | beforeEach(function () { 12 | token = jwt.sign({ nonce: 'abcde' }, 'secret', { algorithm: 'HS256' }); 13 | }); 14 | [ 15 | { 16 | description: 'should work with a string', 17 | nonce: 'abcde', 18 | }, 19 | ].forEach((testCase) => { 20 | it(testCase.description, function (done) { 21 | testUtils.verifyJWTHelper(token, 'secret', { nonce: testCase.nonce }, (err, decoded) => { 22 | testUtils.asyncCheck(done, () => { 23 | expect(err).to.be.null; 24 | expect(decoded).to.have.property('nonce', 'abcde'); 25 | }); 26 | }); 27 | }); 28 | }); 29 | [ 30 | true, 31 | false, 32 | null, 33 | -1, 34 | 0, 35 | 1, 36 | -1.1, 37 | 1.1, 38 | -Infinity, 39 | Infinity, 40 | NaN, 41 | '', 42 | ' ', 43 | [], 44 | ['foo'], 45 | {}, 46 | { foo: 'bar' }, 47 | ].forEach((nonce) => { 48 | it(`should error with value ${util.inspect(nonce)}`, function (done) { 49 | testUtils.verifyJWTHelper(token, 'secret', { nonce }, (err) => { 50 | testUtils.asyncCheck(done, () => { 51 | expect(err).to.be.instanceOf(jwt.JsonWebTokenError); 52 | expect(err).to.have.property('message', 'nonce must be a non-empty string') 53 | }); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/prime256v1-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIMP1Xt/ic2jAHJva2Pll866d1jYL+dk3VdLytEU1+LFmoAoGCCqGSM49 3 | AwEHoUQDQgAEvIywoA1H1a2XpPPTqsRxSk6YnNRVsu4E+wTvb7uV6Yttvko9zWar 4 | jmtM3LHDXk/nHn+Pva0KD+lby8gb2daHGg== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN 3 | +H7GHp3/QhZbSaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXi 4 | c78kOugMY1vng4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6Gb 5 | RKzyTKcB58yx24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRX 6 | kdDSHty6lZWj3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1K 7 | kyHFqWpxaJc/AKf9xgg+UumeqVcirUmAsHJrMwIDAQABAoIBAQCYKw05YSNhXVPk 8 | eHLeW/pXuwR3OkCexPrakOmwMC0s2vIF7mChN0d6hvhVlUp68X7V8SnS2JxAGo8v 9 | iHY+Et3DdwZ3cxnzwh+BEhzgDfoIOmkoGppZPyX/K6klWtbGUrTtSISOWXbvEXQU 10 | G0qGAvDOzIGTsdMDX7slnU70Ac23JybPY5qBSiE+ky8U4dm2fUHMroWub4QP5vA/ 11 | nqyWqX2FB/MEAbcujaknDQrFCtbmtUYlBbJCKGd9V3cGEqp6H7oH+ah2ofMc91gJ 12 | mCHk3YyWZB/bcVXH3CA+s1ywvCOVDBZ3Nw7Pt9zIcv6Rl9UKIy+Nx0QjXxR90Hla 13 | Tr0GHIShAoGBAPsD7uXm+0ksnGyKRYgvlVad8Z8FUFT6bf4B+vboDbx40FO8O/5V 14 | PraBPC5z8YRSBOQ/WfccPQzakkA28F2pXlRpXu5JcErVWnyyUiKpX5sw6iPenQR2 15 | JO9hY/GFbKiwUhVHpvWMcXFqFLSQu2A86jPnFFEfG48ZT4IhTzINKJVZAoGBAMKc 16 | B3YGfVfY9qiRFXzYRdSRLg5c8p/HzuWwXc9vfJ4kQTDkPXe/+nqD67rzeT54uVec 17 | jKoIrsCu4BfEaoyvOT+1KmUfdEpBgYZuuEC4CZf7dgKbXOpPVvZDMyJ/e7HyqTpw 18 | mvIYJLPm2fNAcAsnbrNX5mhLwwzEIltbplUUeRdrAoGBAKhZgPYsLkhrZRXevreR 19 | wkTvdUfD1pbHxtFfHqROCjhnhsFCM7JmFcNtdaFqHYczQxiZ7IqxI7jlNsVek2Md 20 | 3qgaa5LBKlDmOuP67N9WXUrGSaJ5ATIm0qrB1Lf9VlzktIiVH8L7yHHaRby8fQ8U 21 | i7b3ukaV6HPW895A3M6iyJ8xAoGAInp4S+3MaTL0SFsj/nFmtcle6oaHKc3BlyoP 22 | BMBQyMfNkPbu+PdXTjtvGTknouzKkX4X4cwWAec5ppxS8EffEa1sLGxNMxa19vZI 23 | yJaShI21k7Ko3I5f7tNrDNKfPKCsYMEwgnHKluDwfktNTnyW/Uk2dgXuMaXSHHN5 24 | XZt59K8CgYArGVOWK7LUmf3dkTIs3tXBm4/IMtUZmWmcP9C8Xe/Dg/IdQhK5CIx4 25 | VXl8rgZNeX/5/4nJ8Q3LrdLau1Iz620trNRGU6sGMs3x4WQbSq93RRbFzfG1oK74 26 | IOo5yIBxImQOSk5jz31gF9RJb15SDBIxonuWv8qAERyUfvrmEwR0kg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDtTCCAp2gAwIBAgIJAMKR/NsyfcazMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTIxMTEyMjM0MzQxWhcNMTYxMjIxMjM0MzQxWjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN+H7GHp3/ 8 | QhZbSaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXic78kOugM 9 | Y1vng4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6GbRKzyTKcB 10 | 58yx24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRXkdDSHty6 11 | lZWj3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1KkyHFqWpx 12 | aJc/AKf9xgg+UumeqVcirUmAsHJrMwIDAQABo4GnMIGkMB0GA1UdDgQWBBTs83nk 13 | LtoXFlmBUts3EIxcVvkvcjB1BgNVHSMEbjBsgBTs83nkLtoXFlmBUts3EIxcVvkv 14 | cqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV 15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAMKR/NsyfcazMAwGA1UdEwQF 16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABw7w/5k4d5dVDgd/OOOmXdaaCIKvt7d 17 | 3ntlv1SSvAoKT8d8lt97Dm5RrmefBI13I2yivZg5bfTge4+vAV6VdLFdWeFp1b/F 18 | OZkYUv6A8o5HW0OWQYVX26zIqBcG2Qrm3reiSl5BLvpj1WSpCsYvs5kaO4vFpMak 19 | /ICgdZD+rxwxf8Vb/6fntKywWSLgwKH3mJ+Z0kRlpq1g1oieiOm1/gpZ35s0Yuor 20 | XZba9ptfLCYSggg/qc3d3d0tbHplKYkwFm7f5ORGHDSD5SJm+gI7RPE+4bO8q79R 21 | PAfbG1UGuJ0b/oigagciHhJp851SQRYf3JuNSc17BnK2L5IEtzjqr+Q= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /test/rsa-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAvzoCEC2rpSpJQaWZbUmlsDNwp83Jr4fi6KmBWIwnj1MZ6CUQ 3 | 7rBasuLI8AcfX5/10scSfQNCsTLV2tMKQaHuvyrVfwY0dINk+nkqB74QcT2oCCH9 4 | XduJjDuwWA4xLqAKuF96FsIes52opEM50W7/W7DZCKXkC8fFPFj6QF5ZzApDw2Qs 5 | u3yMRmr7/W9uWeaTwfPx24YdY7Ah+fdLy3KN40vXv9c4xiSafVvnx9BwYL7H1Q8N 6 | iK9LGEN6+JSWfgckQCs6UUBOXSZdreNN9zbQCwyzee7bOJqXUDAuLcFARzPw1EsZ 7 | AyjVtGCKIQ0/btqK+jFunT2NBC8RItanDZpptQIDAQABAoIBAQCsssO4Pra8hFMC 8 | gX7tr0x+tAYy1ewmpW8stiDFilYT33YPLKJ9HjHbSms0MwqHftwwTm8JDc/GXmW6 9 | qUui+I64gQOtIzpuW1fvyUtHEMSisI83QRMkF6fCSQm6jJ6oQAtOdZO6R/gYOPNb 10 | 3gayeS8PbMilQcSRSwp6tNTVGyC33p43uUUKAKHnpvAwUSc61aVOtw2wkD062XzM 11 | hJjYpHm65i4V31AzXo8HF42NrAtZ8K/AuQZne5F/6F4QFVlMKzUoHkSUnTp60XZx 12 | X77GuyDeDmCgSc2J7xvR5o6VpjsHMo3ek0gJk5ZBnTgkHvnpbULCRxTmDfjeVPue 13 | v3NN2TBFAoGBAPxbqNEsXPOckGTvG3tUOAAkrK1hfW3TwvrW/7YXg1/6aNV4sklc 14 | vqn/40kCK0v9xJIv9FM/l0Nq+CMWcrb4sjLeGwHAa8ASfk6hKHbeiTFamA6FBkvQ 15 | //7GP5khD+y62RlWi9PmwJY21lEkn2mP99THxqvZjQiAVNiqlYdwiIc7AoGBAMH8 16 | f2Ay7Egc2KYRYU2qwa5E/Cljn/9sdvUnWM+gOzUXpc5sBi+/SUUQT8y/rY4AUVW6 17 | YaK7chG9YokZQq7ZwTCsYxTfxHK2pnG/tXjOxLFQKBwppQfJcFSRLbw0lMbQoZBk 18 | S+zb0ufZzxc2fJfXE+XeJxmKs0TS9ltQuJiSqCPPAoGBALEc84K7DBG+FGmCl1sb 19 | ZKJVGwwknA90zCeYtadrIT0/VkxchWSPvxE5Ep+u8gxHcqrXFTdILjWW4chefOyF 20 | 5ytkTrgQAI+xawxsdyXWUZtd5dJq8lxLtx9srD4gwjh3et8ZqtFx5kCHBCu29Fr2 21 | PA4OmBUMfrs0tlfKgV+pT2j5AoGBAKnA0Z5XMZlxVM0OTH3wvYhI6fk2Kx8TxY2G 22 | nxsh9m3hgcD/mvJRjEaZnZto6PFoqcRBU4taSNnpRr7+kfH8sCht0k7D+l8AIutL 23 | ffx3xHv9zvvGHZqQ1nHKkaEuyjqo+5kli6N8QjWNzsFbdvBQ0CLJoqGhVHsXuWnz 24 | W3Z4cBbVAoGAEtnwY1OJM7+R2u1CW0tTjqDlYU2hUNa9t1AbhyGdI2arYp+p+umA 25 | b5VoYLNsdvZhqjVFTrYNEuhTJFYCF7jAiZLYvYm0C99BqcJnJPl7JjWynoNHNKw3 26 | 9f6PIOE1rAmPE8Cfz/GFF5115ZKVlq+2BY8EKNxbCIy2d/vMEvisnXI= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/rsa-pss-invalid-salt-length-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIE8gIBADBCBgkqhkiG9w0BAQowNaAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZI 3 | hvcNAQEIMA0GCWCGSAFlAwQCAQUAogQCAgQABIIEpzCCBKMCAQACggEBAJy3FuDR 4 | 1qKXsC8o+0xDJbuJCnysT71EFDGQY2/b3cZmxW3rzDYLyE65t2Go1jeK5Kxs+kwS 5 | 1VxfefD8DifeDZN66wjRse4iWLcxmQB5FfishXOdozciimgXNvXJNS8X//feSofl 6 | vDQaTUI0NJnw1qQ2CB0pgGInwajsRKpWnDOhfk3NA/cmGlmfhTtDSTxq0ReytUie 7 | TjY7gy+S9YYm4bAgBcMeoup0GEPzYccK4+1yCmWzQZGFcrY1cuB9bL+vT7ajQFhe 8 | WVKlp6z35GyBF2zI7gJSkHpUHaWV5+Z9aTr6+YP6U7xuCRvXQ/l6BEOUjt4Es2YG 9 | 3frgxeVbOs1gAakCAwEAAQKCAQAMvFxhnOwCfq1Ux9HUWsigOvzdMOuyB+xUMtXB 10 | 625Uh1mYG0eXRNHcg/9BMoVmMiVvVdPphsZMIX45dWJ5HvSffafIKbJ6FdR73s3+ 11 | WdjNQsf9o1v2SRpSZ0CSLO3ji+HDdQ89iBAJc/G/ZZq4v/fRlIqIRC0ozO5SGhFi 12 | fnNnRqH78d2KeJMX/g9jBZM8rJQCi+pb0keHmFmLJ5gZa4HokE8rWQJQY46PVYUH 13 | W2BwEJToMl3MPC7D95soWVuFt3KHnIWhuma/tnCmd2AUvcMrdWq0CwStH3vuX4LB 14 | vJug0toWkobt1tzZgzzCASb2EpzJj8UNxP1CzTQWsvl8OephAoGBAMVnmZeLHoh2 15 | kxn/+rXetZ4Msjgu19MHNQAtlMvqzwZLan0K/BhnHprJLy4SDOuQYIs+PYJuXdT7 16 | Yv2mp9kwTPz8glP9LAto4MDeDfCu0cyXmZb2VQcT/lqVyrwfx3Psqxm/Yxg62YKr 17 | aQE8WqgZGUdOvU9dYU+7EmPlYpdGpPVlAoGBAMs7ks+12oE6kci3WApdnt0kk5+f 18 | 8fbQ0lp2vR3tEw8DURa5FnHWA4o46XvcMcuXwZBrpxANPNAxJJjMBs1hSkc8h4hd 19 | 4vjtRNYJpj+uBdDIRmdqTzbpWv+hv8Xpiol5EVgnMVs2UZWDjoxQ+mYa1R8tAUfj 20 | ojzV2KBMWGCoHgj1AoGALki6JGQEBq72kpQILnhHUQVdC/s/s0TvUlldl+o4HBu2 21 | nhbjQL182YHuQ/kLenfhiwRO27QQ4A0JCrv2gt/mTTLPQ+4KU6qFd/MYhaQXoMay 22 | xkh/aydu7cJNRIqW80E8ZM8Q5u91bEPQXO/PubYYzTVTAba9SDpud2mjEiEIMFkC 23 | gYEAxINEQEgtkkuZ76UpIkzIcjkN7YlxJCFjZUnvL+KvTRL986TgyQ4RujOxwKx4 24 | Ec8ZwZX2opTKOt1p771IzorGkf87ZmayM9TpfLUz5dtVkD43pYOsOQKHlStIDgz2 25 | gltoo/6xwOrTFGlzCsa6eMR1U4Hm/SZlF8IHh2iLBFtLP4kCgYBqTi1XeWeVQVSA 26 | y9Wolv9kMoRh/Xh6F2D8bTTybGshDVO+P4YLM4lLxh5UDZAd/VOkdf3ZIcUGv022 27 | lxrYbLbIEGckMCpkdHeZH/1/iuJUeiCrXeyNlQsXBrmJKr/0lENniJHGpiSEyvY5 28 | D8Oafyjd7ZjUmyBFvS4heQEC6Pjo3Q== 29 | -----END PRIVATE KEY----- 30 | -------------------------------------------------------------------------------- /test/rsa-pss-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIE8QIBADBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZI 3 | hvcNAQEIMA0GCWCGSAFlAwQCAQUAogMCASAEggSnMIIEowIBAAKCAQEA00tEqqyF 4 | VnyvcVA2ewVoSicCMdQXmWyYM82sBWX0wcnn0WUuZp1zjux4xTvQ71Lhx95OJCQZ 5 | 7r7b2192Im5ca37wNRbI6DhyXNdNVFXLFYlNAvgP+V0gIwlr6NgopdJqHCjYVv/g 6 | GOoesRZaDdtV1A3O9CXdJ34x2HZh7nhwYK5hqZDhUW4rd+5GzIIzwCJfwgTQpkIc 7 | 18UeMMEoKJ6A0ixdpf43HqJ5fAB5nsbYFhyHpfiX1UO2EFJtSdbKEIbRmqcbNjG1 8 | tu1tjt6u8LI2coetLh/IYMbMfkyQz+eAUHLQCUb2R8BqLOL3hRqEsVTBo93UJlOs 9 | VWC1fKaq+HOEWQIDAQABAoIBAAet23PagPQTjwAZcAlzjlvs5AMHQsj5gznqwSmR 10 | ut3/e7SGrrOIXbv1iIQejZQ3w8CS/0MH/ttIRiRIaWTh9EDsjvKsU9FAxUNDiJTG 11 | k3LCbTFCQ7kGiJWiu4XDCWMmwmLTRzLjlMjtr/+JS5eSVPcNKMGDI3D9K0xDLSxQ 12 | u0DVigYgWOCWlejHCEU4yi6vBO0HlumWjVPelWb9GmihBDwCLUJtG0JA6H6rw+KS 13 | i6SNXcMGVKfjEghChRp+HaMvLvMgU44Ptnj8jhlfBctXInBY1is1FfDSWxXdVbUM 14 | 1HdKXfV4A50GXSvJLiWP9ZZsaZ7NiBJK8IiJBXD72EFOzwECgYEA3RjnTJn9emzG 15 | 84eIHZQujWWt4Tk/wjeLJYOYtAZpF7R3/fYLVypX9Bsw1IbwZodq/jChTjMaUkYt 16 | //FgUjF/t0uakEg1i+THPZvktNB8Q1E9NwHerB8HF/AD/jMALD+ejdLQ11Z4VScw 17 | zyNmSvD9I84/sgpms5YVKSH9sqww2RkCgYEA9KYws3sTfRLc1hlsS25V6+Zg3ZCk 18 | iGcp+zrxGC1gb2/PpRvEDBucZO21KbSRuQDavWIOZYl4fGu7s8wo2oF8RxOsHQsM 19 | LJyjklruvtjnvuoft/bGAv2zLQkNaj+f7IgK6965gIxcLYL66UPCZZkTfL5CoJis 20 | V0v2hBh1ES5bLUECgYEAuONeaLxNL9dO989akAGefDePFExfePYhshk91S2XLG+J 21 | +CGMkjOioUsrpk3BMrwDSNU5zr8FP8/YH7OlrJYgCxN6CTWZMYb65hY7RskhYNnK 22 | qvkxUBYSRH49mJDlkBsTZ93nLmvs7Kh9NHqRzBGCXjLXKPdxsrPKtj7qfENqBeEC 23 | gYAC9dPXCCE3PTgw2wPlccNWZGY9qBdlkyH96TurmDj3gDnZ/JkFsHvW+M1dYNL2 24 | kx0Sd5JHBj/P+Zm+1jSUWEbBsWo+u7h8/bQ4/CKxanx7YefaWQESXjGB1P81jumH 25 | einvqrVB6fDfmBsjIW/DvPNwafjyaoaDU+b6uDUKbS4rQQKBgCe0pvDl5lO8FM81 26 | NP7GoCIu1gKBS+us1sgYE65ZFmVXJ6b5DckvobXSjM60G2N5w2xaXEXJsnwMApf1 27 | SClQUsgNWcSXRwL+w0pIdyFKS25BSfwUNQ9n7QLJcYgmflbARTfB3He/10vbFzTp 28 | G6ZAiKUp9bKFPzviII40AEPL2hPX 29 | -----END PRIVATE KEY----- 30 | -------------------------------------------------------------------------------- /test/rsa-public-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PUBLIC KEY----- 2 | MIIBCgKCAQEAvzoCEC2rpSpJQaWZbUmlsDNwp83Jr4fi6KmBWIwnj1MZ6CUQ7rBa 3 | suLI8AcfX5/10scSfQNCsTLV2tMKQaHuvyrVfwY0dINk+nkqB74QcT2oCCH9XduJ 4 | jDuwWA4xLqAKuF96FsIes52opEM50W7/W7DZCKXkC8fFPFj6QF5ZzApDw2Qsu3yM 5 | Rmr7/W9uWeaTwfPx24YdY7Ah+fdLy3KN40vXv9c4xiSafVvnx9BwYL7H1Q8NiK9L 6 | GEN6+JSWfgckQCs6UUBOXSZdreNN9zbQCwyzee7bOJqXUDAuLcFARzPw1EsZAyjV 7 | tGCKIQ0/btqK+jFunT2NBC8RItanDZpptQIDAQAB 8 | -----END RSA PUBLIC KEY----- 9 | -------------------------------------------------------------------------------- /test/rsa-public-key.tests.js: -------------------------------------------------------------------------------- 1 | const jwt = require('../'); 2 | const PS_SUPPORTED = require('../lib/psSupported'); 3 | const expect = require('chai').expect; 4 | const {generateKeyPairSync} = require('crypto') 5 | 6 | describe('public key start with BEGIN RSA PUBLIC KEY', function () { 7 | 8 | it('should work for RS family of algorithms', function (done) { 9 | var fs = require('fs'); 10 | var cert_pub = fs.readFileSync(__dirname + '/rsa-public-key.pem'); 11 | var cert_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); 12 | 13 | var token = jwt.sign({ foo: 'bar' }, cert_priv, { algorithm: 'RS256'}); 14 | 15 | jwt.verify(token, cert_pub, done); 16 | }); 17 | 18 | it('should not work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is false or not set', function (done) { 19 | const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); 20 | 21 | expect(function() { 22 | jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256'}) 23 | }).to.throw(Error, 'minimum key size'); 24 | 25 | done() 26 | }); 27 | 28 | it('should work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is true', function (done) { 29 | const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); 30 | 31 | jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256', allowInsecureKeySizes: true}, done) 32 | }); 33 | 34 | if (PS_SUPPORTED) { 35 | it('should work for PS family of algorithms', function (done) { 36 | var fs = require('fs'); 37 | var cert_pub = fs.readFileSync(__dirname + '/rsa-public-key.pem'); 38 | var cert_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); 39 | 40 | var token = jwt.sign({ foo: 'bar' }, cert_priv, { algorithm: 'PS256'}); 41 | 42 | jwt.verify(token, cert_pub, done); 43 | }); 44 | } 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/rsa-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvzoCEC2rpSpJQaWZbUml 3 | sDNwp83Jr4fi6KmBWIwnj1MZ6CUQ7rBasuLI8AcfX5/10scSfQNCsTLV2tMKQaHu 4 | vyrVfwY0dINk+nkqB74QcT2oCCH9XduJjDuwWA4xLqAKuF96FsIes52opEM50W7/ 5 | W7DZCKXkC8fFPFj6QF5ZzApDw2Qsu3yMRmr7/W9uWeaTwfPx24YdY7Ah+fdLy3KN 6 | 40vXv9c4xiSafVvnx9BwYL7H1Q8NiK9LGEN6+JSWfgckQCs6UUBOXSZdreNN9zbQ 7 | Cwyzee7bOJqXUDAuLcFARzPw1EsZAyjVtGCKIQ0/btqK+jFunT2NBC8RItanDZpp 8 | tQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/schema.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | var fs = require('fs'); 4 | var PS_SUPPORTED = require('../lib/psSupported'); 5 | 6 | describe('schema', function() { 7 | 8 | describe('sign options', function() { 9 | var cert_rsa_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); 10 | var cert_ecdsa_priv = fs.readFileSync(__dirname + '/ecdsa-private.pem'); 11 | var cert_secp384r1_priv = fs.readFileSync(__dirname + '/secp384r1-private.pem'); 12 | var cert_secp521r1_priv = fs.readFileSync(__dirname + '/secp521r1-private.pem'); 13 | 14 | function sign(options, secretOrPrivateKey) { 15 | jwt.sign({foo: 123}, secretOrPrivateKey, options); 16 | } 17 | 18 | it('should validate algorithm', function () { 19 | expect(function () { 20 | sign({ algorithm: 'foo' }, cert_rsa_priv); 21 | }).to.throw(/"algorithm" must be a valid string enum value/); 22 | sign({ algorithm: 'none' }, null); 23 | sign({algorithm: 'RS256'}, cert_rsa_priv); 24 | sign({algorithm: 'RS384'}, cert_rsa_priv); 25 | sign({algorithm: 'RS512'}, cert_rsa_priv); 26 | if (PS_SUPPORTED) { 27 | sign({algorithm: 'PS256'}, cert_rsa_priv); 28 | sign({algorithm: 'PS384'}, cert_rsa_priv); 29 | sign({algorithm: 'PS512'}, cert_rsa_priv); 30 | } 31 | sign({algorithm: 'ES256'}, cert_ecdsa_priv); 32 | sign({algorithm: 'ES384'}, cert_secp384r1_priv); 33 | sign({algorithm: 'ES512'}, cert_secp521r1_priv); 34 | sign({algorithm: 'HS256'}, 'superSecret'); 35 | sign({algorithm: 'HS384'}, 'superSecret'); 36 | sign({algorithm: 'HS512'}, 'superSecret'); 37 | }); 38 | 39 | it('should validate header', function () { 40 | expect(function () { 41 | sign({ header: 'foo' }, 'superSecret'); 42 | }).to.throw(/"header" must be an object/); 43 | sign({header: {}}, 'superSecret'); 44 | }); 45 | 46 | it('should validate encoding', function () { 47 | expect(function () { 48 | sign({ encoding: 10 }, 'superSecret'); 49 | }).to.throw(/"encoding" must be a string/); 50 | sign({encoding: 'utf8'},'superSecret'); 51 | }); 52 | 53 | it('should validate noTimestamp', function () { 54 | expect(function () { 55 | sign({ noTimestamp: 10 }, 'superSecret'); 56 | }).to.throw(/"noTimestamp" must be a boolean/); 57 | sign({noTimestamp: true}, 'superSecret'); 58 | }); 59 | }); 60 | 61 | describe('sign payload registered claims', function() { 62 | 63 | function sign(payload) { 64 | jwt.sign(payload, 'foo123'); 65 | } 66 | 67 | it('should validate exp', function () { 68 | expect(function () { 69 | sign({ exp: '1 monkey' }); 70 | }).to.throw(/"exp" should be a number of seconds/); 71 | sign({ exp: 10.1 }); 72 | }); 73 | 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /test/secp384r1-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDCez58vZHVp+ArI7/fe835GAtRzE0AtrxGgQAY1U/uk2SQOaSw1ph61 3 | 3Unr0ygS172gBwYFK4EEACKhZANiAARtwlnIqYqZxfiWR+/EM35nKHuLpOjUHiX1 4 | kEpSS03C9XlrBLNwLQfgjpYx9Qvqh26XAzTe74DYjcc748R+zZD2YAd3lV+OcdRE 5 | U+DWm4j5E6dlOXzvmw/3qxUcg3rRgR4= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /test/secp521r1-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIHcAgEBBEIBlWXKBKKCgTgf7+NS09TMv7/NO3RtMBn9xTe+46oNNNK405lrZ9mz 3 | WYtlsYvkdsc2Cx3v5V8JegaCOM+XtAZ0MNKgBwYFK4EEACOhgYkDgYYABAFNzaM7 4 | Zb9ug0p5KaZb5mjHrIshoVJSHaOXGtcjLVUakYVk0v9VsE+FKqyuLYcORUuAZdxl 5 | ITAlC5e5JZ0o8NEKbAE+8oOrePrItR3IFBtWO15p7qiRa2dBB8oQklFrmQaJYn4K 6 | fDV0hYpfu6ahpRNu2akR7aMXL/vXrptCH/n64q9KjA== 7 | -----END EC PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/set_headers.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var expect = require('chai').expect; 3 | 4 | describe('set header', function() { 5 | 6 | it('should add the header', function () { 7 | var token = jwt.sign({foo: 123}, '123', { header: { foo: 'bar' } }); 8 | var decoded = jwt.decode(token, {complete: true}); 9 | expect(decoded.header.foo).to.equal('bar'); 10 | }); 11 | 12 | it('should allow overriding header', function () { 13 | var token = jwt.sign({foo: 123}, '123', { header: { alg: 'HS512' } }); 14 | var decoded = jwt.decode(token, {complete: true}); 15 | expect(decoded.header.alg).to.equal('HS512'); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('../'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | 7 | /** 8 | * Correctly report errors that occur in an asynchronous callback 9 | * @param {function(err): void} done The mocha callback 10 | * @param {function(): void} testFunction The assertions function 11 | */ 12 | function asyncCheck(done, testFunction) { 13 | try { 14 | testFunction(); 15 | done(); 16 | } 17 | catch(err) { 18 | done(err); 19 | } 20 | } 21 | 22 | /** 23 | * Assert that two errors are equal 24 | * @param e1 {Error} The first error 25 | * @param e2 {Error} The second error 26 | */ 27 | // chai does not do deep equality on errors: https://github.com/chaijs/chai/issues/1009 28 | function expectEqualError(e1, e2) { 29 | // message and name are not always enumerable, so manually reference them 30 | expect(e1.message, 'Async/Sync Error equality: message').to.equal(e2.message); 31 | expect(e1.name, 'Async/Sync Error equality: name').to.equal(e2.name); 32 | 33 | // compare other enumerable error properties 34 | for(const propertyName in e1) { 35 | expect(e1[propertyName], `Async/Sync Error equality: ${propertyName}`).to.deep.equal(e2[propertyName]); 36 | } 37 | } 38 | 39 | /** 40 | * Base64-url encode a string 41 | * @param str {string} The string to encode 42 | * @returns {string} The encoded string 43 | */ 44 | function base64UrlEncode(str) { 45 | return Buffer.from(str).toString('base64') 46 | .replace(/[=]/g, "") 47 | .replace(/\+/g, "-") 48 | .replace(/\//g, "_") 49 | ; 50 | } 51 | 52 | /** 53 | * Verify a JWT, ensuring that the asynchronous and synchronous calls to `verify` have the same result 54 | * @param {string} jwtString The JWT as a string 55 | * @param {string} secretOrPrivateKey The shared secret or private key 56 | * @param {object} options Verify options 57 | * @param {function(err, token):void} callback 58 | */ 59 | function verifyJWTHelper(jwtString, secretOrPrivateKey, options, callback) { 60 | // freeze the time to ensure the clock remains stable across the async and sync calls 61 | const fakeClock = sinon.useFakeTimers({now: Date.now()}); 62 | let error; 63 | let syncVerified; 64 | try { 65 | syncVerified = jwt.verify(jwtString, secretOrPrivateKey, options); 66 | } 67 | catch (err) { 68 | error = err; 69 | } 70 | jwt.verify(jwtString, secretOrPrivateKey, options, (err, asyncVerifiedToken) => { 71 | try { 72 | if (error) { 73 | expectEqualError(err, error); 74 | callback(err); 75 | } 76 | else { 77 | expect(syncVerified, 'Async/Sync token equality').to.deep.equal(asyncVerifiedToken); 78 | callback(null, syncVerified); 79 | } 80 | } 81 | finally { 82 | if (fakeClock) { 83 | fakeClock.restore(); 84 | } 85 | } 86 | }); 87 | } 88 | 89 | /** 90 | * Sign a payload to create a JWT, ensuring that the asynchronous and synchronous calls to `sign` have the same result 91 | * @param {object} payload The JWT payload 92 | * @param {string} secretOrPrivateKey The shared secret or private key 93 | * @param {object} options Sign options 94 | * @param {function(err, token):void} callback 95 | */ 96 | function signJWTHelper(payload, secretOrPrivateKey, options, callback) { 97 | // freeze the time to ensure the clock remains stable across the async and sync calls 98 | const fakeClock = sinon.useFakeTimers({now: Date.now()}); 99 | let error; 100 | let syncSigned; 101 | try { 102 | syncSigned = jwt.sign(payload, secretOrPrivateKey, options); 103 | } 104 | catch (err) { 105 | error = err; 106 | } 107 | jwt.sign(payload, secretOrPrivateKey, options, (err, asyncSigned) => { 108 | fakeClock.restore(); 109 | if (error) { 110 | expectEqualError(err, error); 111 | callback(err); 112 | } 113 | else { 114 | expect(syncSigned, 'Async/Sync token equality').to.equal(asyncSigned); 115 | callback(null, syncSigned); 116 | } 117 | }); 118 | } 119 | 120 | module.exports = { 121 | asyncCheck, 122 | base64UrlEncode, 123 | signJWTHelper, 124 | verifyJWTHelper, 125 | }; 126 | -------------------------------------------------------------------------------- /test/undefined_secretOrPublickey.tests.js: -------------------------------------------------------------------------------- 1 | var jwt = require('../index'); 2 | var JsonWebTokenError = require('../lib/JsonWebTokenError'); 3 | var expect = require('chai').expect; 4 | 5 | var TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M'; 6 | 7 | describe('verifying without specified secret or public key', function () { 8 | it('should not verify null', function () { 9 | expect(function () { 10 | jwt.verify(TOKEN, null); 11 | }).to.throw(JsonWebTokenError, /secret or public key must be provided/); 12 | }); 13 | 14 | it('should not verify undefined', function () { 15 | expect(function () { 16 | jwt.verify(TOKEN); 17 | }).to.throw(JsonWebTokenError, /secret or public key must be provided/); 18 | }); 19 | }); -------------------------------------------------------------------------------- /test/validateAsymmetricKey.tests.js: -------------------------------------------------------------------------------- 1 | const validateAsymmetricKey = require('../lib/validateAsymmetricKey'); 2 | const PS_SUPPORTED = require('../lib/psSupported'); 3 | const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('../lib/asymmetricKeyDetailsSupported'); 4 | const RSA_PSS_KEY_DETAILS_SUPPORTED = require('../lib/rsaPssKeyDetailsSupported'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const { createPrivateKey } = require('crypto'); 8 | const expect = require('chai').expect; 9 | 10 | function loadKey(filename) { 11 | return createPrivateKey( 12 | fs.readFileSync(path.join(__dirname, filename)) 13 | ); 14 | } 15 | 16 | const algorithmParams = { 17 | RS256: { 18 | invalidPrivateKey: loadKey('secp384r1-private.pem') 19 | }, 20 | ES256: { 21 | invalidPrivateKey: loadKey('priv.pem') 22 | } 23 | }; 24 | 25 | if (PS_SUPPORTED) { 26 | algorithmParams.PS256 = { 27 | invalidPrivateKey: loadKey('secp384r1-private.pem') 28 | }; 29 | } 30 | 31 | describe('Asymmetric key validation', function() { 32 | Object.keys(algorithmParams).forEach(function(algorithm) { 33 | describe(algorithm, function() { 34 | const keys = algorithmParams[algorithm]; 35 | 36 | describe('when validating a key with an invalid private key type', function () { 37 | it('should throw an error', function () { 38 | const expectedErrorMessage = /"alg" parameter for "[\w\d-]+" key type must be one of:/; 39 | 40 | expect(function() { 41 | validateAsymmetricKey(algorithm, keys.invalidPrivateKey); 42 | }).to.throw(expectedErrorMessage); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('when the function has missing parameters', function() { 49 | it('should pass the validation if no key has been provided', function() { 50 | const algorithm = 'ES256'; 51 | validateAsymmetricKey(algorithm); 52 | }); 53 | 54 | it('should pass the validation if no algorithm has been provided', function() { 55 | const key = loadKey('dsa-private.pem'); 56 | validateAsymmetricKey(null, key); 57 | }); 58 | }); 59 | 60 | describe('when validating a key with an unsupported type', function () { 61 | it('should throw an error', function() { 62 | const algorithm = 'RS256'; 63 | const key = loadKey('dsa-private.pem'); 64 | const expectedErrorMessage = 'Unknown key type "dsa".'; 65 | 66 | expect(function() { 67 | validateAsymmetricKey(algorithm, key); 68 | }).to.throw(expectedErrorMessage); 69 | }); 70 | }); 71 | 72 | describe('Elliptic curve algorithms', function () { 73 | const curvesAlgorithms = [ 74 | { algorithm: 'ES256', curve: 'prime256v1' }, 75 | { algorithm: 'ES384', curve: 'secp384r1' }, 76 | { algorithm: 'ES512', curve: 'secp521r1' }, 77 | ]; 78 | 79 | const curvesKeys = [ 80 | { curve: 'prime256v1', key: loadKey('prime256v1-private.pem') }, 81 | { curve: 'secp384r1', key: loadKey('secp384r1-private.pem') }, 82 | { curve: 'secp521r1', key: loadKey('secp521r1-private.pem') } 83 | ]; 84 | 85 | describe('when validating keys generated using Elliptic Curves', function () { 86 | curvesAlgorithms.forEach(function(curveAlgorithm) { 87 | curvesKeys 88 | .forEach((curveKeys) => { 89 | if (curveKeys.curve !== curveAlgorithm.curve) { 90 | if (ASYMMETRIC_KEY_DETAILS_SUPPORTED) { 91 | it(`should throw an error when validating an ${curveAlgorithm.algorithm} token for key with curve ${curveKeys.curve}`, function() { 92 | expect(() => { 93 | validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); 94 | }).to.throw(`"alg" parameter "${curveAlgorithm.algorithm}" requires curve "${curveAlgorithm.curve}".`); 95 | }); 96 | } else { 97 | it(`should pass the validation for incorrect keys if the Node version does not support checking the key's curve name`, function() { 98 | expect(() => { 99 | validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); 100 | }).not.to.throw(); 101 | }); 102 | } 103 | } else { 104 | it(`should accept an ${curveAlgorithm.algorithm} token for key with curve ${curveKeys.curve}`, function() { 105 | expect(() => { 106 | validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); 107 | }).not.to.throw(); 108 | }); 109 | } 110 | }); 111 | }); 112 | }); 113 | }); 114 | 115 | if (RSA_PSS_KEY_DETAILS_SUPPORTED) { 116 | describe('RSA-PSS algorithms', function () { 117 | const key = loadKey('rsa-pss-private.pem'); 118 | 119 | it(`it should throw an error when validating a key with wrong RSA-RSS parameters`, function () { 120 | const algorithm = 'PS512'; 121 | expect(function() { 122 | validateAsymmetricKey(algorithm, key); 123 | }).to.throw('Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS512') 124 | }); 125 | 126 | it(`it should throw an error when validating a key with invalid salt length`, function () { 127 | const algorithm = 'PS256'; 128 | const shortSaltKey = loadKey('rsa-pss-invalid-salt-length-private.pem'); 129 | expect(function() { 130 | validateAsymmetricKey(algorithm, shortSaltKey); 131 | }).to.throw('Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" PS256.') 132 | }); 133 | 134 | it(`it should pass the validation when the key matches all the requirements for the algorithm`, function () { 135 | expect(function() { 136 | const algorithm = 'PS256'; 137 | validateAsymmetricKey(algorithm, key); 138 | }).not.to.throw() 139 | }); 140 | }); 141 | } 142 | }); 143 | -------------------------------------------------------------------------------- /test/verify.tests.js: -------------------------------------------------------------------------------- 1 | const jwt = require('../index'); 2 | const jws = require('jws'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const sinon = require('sinon'); 6 | const JsonWebTokenError = require('../lib/JsonWebTokenError'); 7 | 8 | const assert = require('chai').assert; 9 | const expect = require('chai').expect; 10 | 11 | describe('verify', function() { 12 | const pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); 13 | const priv = fs.readFileSync(path.join(__dirname, 'priv.pem')); 14 | 15 | it('should first assume JSON claim set', function (done) { 16 | const header = { alg: 'RS256' }; 17 | const payload = { iat: Math.floor(Date.now() / 1000 ) }; 18 | 19 | const signed = jws.sign({ 20 | header: header, 21 | payload: payload, 22 | secret: priv, 23 | encoding: 'utf8' 24 | }); 25 | 26 | jwt.verify(signed, pub, {typ: 'JWT'}, function(err, p) { 27 | assert.isNull(err); 28 | assert.deepEqual(p, payload); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('should not be able to verify unsigned token', function () { 34 | const header = { alg: 'none' }; 35 | const payload = { iat: Math.floor(Date.now() / 1000 ) }; 36 | 37 | const signed = jws.sign({ 38 | header: header, 39 | payload: payload, 40 | secret: 'secret', 41 | encoding: 'utf8' 42 | }); 43 | 44 | expect(function () { 45 | jwt.verify(signed, 'secret', {typ: 'JWT'}); 46 | }).to.throw(JsonWebTokenError, /jwt signature is required/); 47 | }); 48 | 49 | it('should not be able to verify unsigned token', function () { 50 | const header = { alg: 'none' }; 51 | const payload = { iat: Math.floor(Date.now() / 1000 ) }; 52 | 53 | const signed = jws.sign({ 54 | header: header, 55 | payload: payload, 56 | secret: 'secret', 57 | encoding: 'utf8' 58 | }); 59 | 60 | expect(function () { 61 | jwt.verify(signed, undefined, {typ: 'JWT'}); 62 | }).to.throw(JsonWebTokenError, /please specify "none" in "algorithms" to verify unsigned tokens/); 63 | }); 64 | 65 | it('should be able to verify unsigned token when none is specified', function (done) { 66 | const header = { alg: 'none' }; 67 | const payload = { iat: Math.floor(Date.now() / 1000 ) }; 68 | 69 | const signed = jws.sign({ 70 | header: header, 71 | payload: payload, 72 | secret: 'secret', 73 | encoding: 'utf8' 74 | }); 75 | 76 | jwt.verify(signed, null, {typ: 'JWT', algorithms: ['none']}, function(err, p) { 77 | assert.isNull(err); 78 | assert.deepEqual(p, payload); 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should not mutate options', function (done) { 84 | const header = { alg: 'HS256' }; 85 | const payload = { iat: Math.floor(Date.now() / 1000 ) }; 86 | const options = { typ: 'JWT' }; 87 | const signed = jws.sign({ 88 | header: header, 89 | payload: payload, 90 | secret: 'secret', 91 | encoding: 'utf8' 92 | }); 93 | 94 | jwt.verify(signed, 'secret', options, function(err) { 95 | assert.isNull(err); 96 | assert.deepEqual(Object.keys(options).length, 1); 97 | done(); 98 | }); 99 | }); 100 | 101 | describe('secret or token as callback', function () { 102 | const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; 103 | const key = 'key'; 104 | 105 | const payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 }; 106 | const options = {algorithms: ['HS256'], ignoreExpiration: true}; 107 | 108 | it('without callback', function (done) { 109 | jwt.verify(token, key, options, function (err, p) { 110 | assert.isNull(err); 111 | assert.deepEqual(p, payload); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('simple callback', function (done) { 117 | const keyFunc = function(header, callback) { 118 | assert.deepEqual(header, { alg: 'HS256', typ: 'JWT' }); 119 | 120 | callback(undefined, key); 121 | }; 122 | 123 | jwt.verify(token, keyFunc, options, function (err, p) { 124 | assert.isNull(err); 125 | assert.deepEqual(p, payload); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('should error if called synchronously', function (done) { 131 | const keyFunc = function(header, callback) { 132 | callback(undefined, key); 133 | }; 134 | 135 | expect(function () { 136 | jwt.verify(token, keyFunc, options); 137 | }).to.throw(JsonWebTokenError, /verify must be called asynchronous if secret or public key is provided as a callback/); 138 | 139 | done(); 140 | }); 141 | 142 | it('simple error', function (done) { 143 | const keyFunc = function(header, callback) { 144 | callback(new Error('key not found')); 145 | }; 146 | 147 | jwt.verify(token, keyFunc, options, function (err, p) { 148 | assert.equal(err.name, 'JsonWebTokenError'); 149 | assert.match(err.message, /error in secret or public key callback/); 150 | assert.isUndefined(p); 151 | done(); 152 | }); 153 | }); 154 | 155 | it('delayed callback', function (done) { 156 | const keyFunc = function(header, callback) { 157 | setTimeout(function() { 158 | callback(undefined, key); 159 | }, 25); 160 | }; 161 | 162 | jwt.verify(token, keyFunc, options, function (err, p) { 163 | assert.isNull(err); 164 | assert.deepEqual(p, payload); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('delayed error', function (done) { 170 | const keyFunc = function(header, callback) { 171 | setTimeout(function() { 172 | callback(new Error('key not found')); 173 | }, 25); 174 | }; 175 | 176 | jwt.verify(token, keyFunc, options, function (err, p) { 177 | assert.equal(err.name, 'JsonWebTokenError'); 178 | assert.match(err.message, /error in secret or public key callback/); 179 | assert.isUndefined(p); 180 | done(); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('expiration', function () { 186 | // { foo: 'bar', iat: 1437018582, exp: 1437018592 } 187 | const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; 188 | const key = 'key'; 189 | 190 | let clock; 191 | afterEach(function () { 192 | try { clock.restore(); } catch (e) {} 193 | }); 194 | 195 | it('should error on expired token', function (done) { 196 | clock = sinon.useFakeTimers(1437018650000); // iat + 58s, exp + 48s 197 | const options = {algorithms: ['HS256']}; 198 | 199 | jwt.verify(token, key, options, function (err, p) { 200 | assert.equal(err.name, 'TokenExpiredError'); 201 | assert.equal(err.message, 'jwt expired'); 202 | assert.equal(err.expiredAt.constructor.name, 'Date'); 203 | assert.equal(Number(err.expiredAt), 1437018592000); 204 | assert.isUndefined(p); 205 | done(); 206 | }); 207 | }); 208 | 209 | it('should not error on expired token within clockTolerance interval', function (done) { 210 | clock = sinon.useFakeTimers(1437018594000); // iat + 12s, exp + 2s 211 | const options = {algorithms: ['HS256'], clockTolerance: 5 } 212 | 213 | jwt.verify(token, key, options, function (err, p) { 214 | assert.isNull(err); 215 | assert.equal(p.foo, 'bar'); 216 | done(); 217 | }); 218 | }); 219 | 220 | describe('option: clockTimestamp', function () { 221 | const clockTimestamp = 1000000000; 222 | it('should verify unexpired token relative to user-provided clockTimestamp', function (done) { 223 | const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); 224 | jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err) { 225 | assert.isNull(err); 226 | done(); 227 | }); 228 | }); 229 | it('should error on expired token relative to user-provided clockTimestamp', function (done) { 230 | const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); 231 | jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) { 232 | assert.equal(err.name, 'TokenExpiredError'); 233 | assert.equal(err.message, 'jwt expired'); 234 | assert.equal(err.expiredAt.constructor.name, 'Date'); 235 | assert.equal(Number(err.expiredAt), (clockTimestamp + 1) * 1000); 236 | assert.isUndefined(p); 237 | done(); 238 | }); 239 | }); 240 | it('should verify clockTimestamp is a number', function (done) { 241 | const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); 242 | jwt.verify(token, key, {clockTimestamp: 'notANumber'}, function (err, p) { 243 | assert.equal(err.name, 'JsonWebTokenError'); 244 | assert.equal(err.message,'clockTimestamp must be a number'); 245 | assert.isUndefined(p); 246 | done(); 247 | }); 248 | }); 249 | }); 250 | 251 | describe('option: maxAge and clockTimestamp', function () { 252 | // { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s 253 | const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; 254 | it('cannot be more permissive than expiration', function (done) { 255 | const clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s) 256 | const options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'}; 257 | 258 | jwt.verify(token, key, options, function (err, p) { 259 | // maxAge not exceded, but still expired 260 | assert.equal(err.name, 'TokenExpiredError'); 261 | assert.equal(err.message, 'jwt expired'); 262 | assert.equal(err.expiredAt.constructor.name, 'Date'); 263 | assert.equal(Number(err.expiredAt), 1437018800000); 264 | assert.isUndefined(p); 265 | done(); 266 | }); 267 | }); 268 | }); 269 | }); 270 | 271 | describe('when verifying a token with an unsupported public key type', function () { 272 | it('should throw an error', function() { 273 | const token = 'eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2Njk5OTAwMDN9.YdjFWJtPg_9nccMnTfQyesWQ0UX-GsWrfCGit_HqjeIkNjoV6dkAJ8AtbnVEhA4oxwqSXx6ilMOfHEjmMlPtyyyVKkWKQHcIWYnqPbNSEv8a7Men8KhJTIWb4sf5YbhgSCpNvU_VIZjLO1Z0PzzgmEikp0vYbxZFAbCAlZCvUlcIc-kdjIRCnDJe0BBrYRxNLEJtYsf7D1yFIFIqw8-VP87yZdExA4eHsTaE84SgnL24ZK5h5UooDx-IRNd_rrMyio8kNy63grVxCWOtkXZ26iZk6v-HMsnBqxvUwR6-8wfaWrcpADkyUO1q3SNsoTdwtflbvfwgjo3uve0IvIzHMw'; 274 | const key = fs.readFileSync(path.join(__dirname, 'dsa-public.pem')); 275 | 276 | expect(function() { 277 | jwt.verify(token, key); 278 | }).to.throw('Unknown key type "dsa".'); 279 | }); 280 | }); 281 | 282 | describe('when verifying a token with an incorrect public key type', function () { 283 | it('should throw a validation error if key validation is enabled', function() { 284 | const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXkiOiJsb2FkIiwiaWF0IjoxNjcwMjMwNDE2fQ.7TYP8SB_9Tw1fNIfuG60b4tvoLPpDAVBQpV1oepnuKwjUz8GOw4fRLzclo0Q2YAXisJ3zIYMEFsHpYrflfoZJQ'; 285 | const key = fs.readFileSync(path.join(__dirname, 'rsa-public.pem')); 286 | 287 | expect(function() { 288 | jwt.verify(token, key, { algorithms: ['ES256'] }); 289 | }).to.throw('"alg" parameter for "rsa" key type must be one of: RS256, PS256, RS384, PS384, RS512, PS512.'); 290 | }); 291 | 292 | it('should throw an unknown error if key validation is disabled', function() { 293 | const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXkiOiJsb2FkIiwiaWF0IjoxNjcwMjMwNDE2fQ.7TYP8SB_9Tw1fNIfuG60b4tvoLPpDAVBQpV1oepnuKwjUz8GOw4fRLzclo0Q2YAXisJ3zIYMEFsHpYrflfoZJQ'; 294 | const key = fs.readFileSync(path.join(__dirname, 'rsa-public.pem')); 295 | 296 | expect(function() { 297 | jwt.verify(token, key, { algorithms: ['ES256'], allowInvalidAsymmetricKeyTypes: true }); 298 | }).to.not.throw('"alg" parameter for "rsa" key type must be one of: RS256, PS256, RS384, PS384, RS512, PS512.'); 299 | }); 300 | }); 301 | }); 302 | -------------------------------------------------------------------------------- /test/wrong_alg.tests.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var jwt = require('../index'); 4 | var JsonWebTokenError = require('../lib/JsonWebTokenError'); 5 | var PS_SUPPORTED = require('../lib/psSupported'); 6 | var expect = require('chai').expect; 7 | 8 | 9 | var pub = fs.readFileSync(path.join(__dirname, 'pub.pem'), 'utf8'); 10 | // priv is never used 11 | // var priv = fs.readFileSync(path.join(__dirname, 'priv.pem')); 12 | 13 | var TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MjY1NDY5MTl9.ETgkTn8BaxIX4YqvUWVFPmum3moNZ7oARZtSBXb_vP4'; 14 | 15 | describe('when setting a wrong `header.alg`', function () { 16 | 17 | describe('signing with pub key as symmetric', function () { 18 | it('should not verify', function () { 19 | expect(function () { 20 | jwt.verify(TOKEN, pub); 21 | }).to.throw(JsonWebTokenError, /invalid algorithm/); 22 | }); 23 | }); 24 | 25 | describe('signing with pub key as HS256 and whitelisting only RS256', function () { 26 | it('should not verify', function () { 27 | expect(function () { 28 | jwt.verify(TOKEN, pub, {algorithms: ['RS256']}); 29 | }).to.throw(JsonWebTokenError, /invalid algorithm/); 30 | }); 31 | }); 32 | 33 | if (PS_SUPPORTED) { 34 | describe('signing with pub key as HS256 and whitelisting only PS256', function () { 35 | it('should not verify', function () { 36 | expect(function () { 37 | jwt.verify(TOKEN, pub, {algorithms: ['PS256']}); 38 | }).to.throw(JsonWebTokenError, /invalid algorithm/); 39 | }); 40 | }); 41 | } 42 | 43 | describe('signing with HS256 and checking with HS384', function () { 44 | it('should not verify', function () { 45 | expect(function () { 46 | var token = jwt.sign({foo: 'bar'}, 'secret', {algorithm: 'HS256'}); 47 | jwt.verify(token, 'some secret', {algorithms: ['HS384']}); 48 | }).to.throw(JsonWebTokenError, /invalid algorithm/); 49 | }); 50 | }); 51 | 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /verify.js: -------------------------------------------------------------------------------- 1 | const JsonWebTokenError = require('./lib/JsonWebTokenError'); 2 | const NotBeforeError = require('./lib/NotBeforeError'); 3 | const TokenExpiredError = require('./lib/TokenExpiredError'); 4 | const decode = require('./decode'); 5 | const timespan = require('./lib/timespan'); 6 | const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); 7 | const PS_SUPPORTED = require('./lib/psSupported'); 8 | const jws = require('jws'); 9 | const {KeyObject, createSecretKey, createPublicKey} = require("crypto"); 10 | 11 | const PUB_KEY_ALGS = ['RS256', 'RS384', 'RS512']; 12 | const EC_KEY_ALGS = ['ES256', 'ES384', 'ES512']; 13 | const RSA_KEY_ALGS = ['RS256', 'RS384', 'RS512']; 14 | const HS_ALGS = ['HS256', 'HS384', 'HS512']; 15 | 16 | if (PS_SUPPORTED) { 17 | PUB_KEY_ALGS.splice(PUB_KEY_ALGS.length, 0, 'PS256', 'PS384', 'PS512'); 18 | RSA_KEY_ALGS.splice(RSA_KEY_ALGS.length, 0, 'PS256', 'PS384', 'PS512'); 19 | } 20 | 21 | module.exports = function (jwtString, secretOrPublicKey, options, callback) { 22 | if ((typeof options === 'function') && !callback) { 23 | callback = options; 24 | options = {}; 25 | } 26 | 27 | if (!options) { 28 | options = {}; 29 | } 30 | 31 | //clone this object since we are going to mutate it. 32 | options = Object.assign({}, options); 33 | 34 | let done; 35 | 36 | if (callback) { 37 | done = callback; 38 | } else { 39 | done = function(err, data) { 40 | if (err) throw err; 41 | return data; 42 | }; 43 | } 44 | 45 | if (options.clockTimestamp && typeof options.clockTimestamp !== 'number') { 46 | return done(new JsonWebTokenError('clockTimestamp must be a number')); 47 | } 48 | 49 | if (options.nonce !== undefined && (typeof options.nonce !== 'string' || options.nonce.trim() === '')) { 50 | return done(new JsonWebTokenError('nonce must be a non-empty string')); 51 | } 52 | 53 | if (options.allowInvalidAsymmetricKeyTypes !== undefined && typeof options.allowInvalidAsymmetricKeyTypes !== 'boolean') { 54 | return done(new JsonWebTokenError('allowInvalidAsymmetricKeyTypes must be a boolean')); 55 | } 56 | 57 | const clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000); 58 | 59 | if (!jwtString){ 60 | return done(new JsonWebTokenError('jwt must be provided')); 61 | } 62 | 63 | if (typeof jwtString !== 'string') { 64 | return done(new JsonWebTokenError('jwt must be a string')); 65 | } 66 | 67 | const parts = jwtString.split('.'); 68 | 69 | if (parts.length !== 3){ 70 | return done(new JsonWebTokenError('jwt malformed')); 71 | } 72 | 73 | let decodedToken; 74 | 75 | try { 76 | decodedToken = decode(jwtString, { complete: true }); 77 | } catch(err) { 78 | return done(err); 79 | } 80 | 81 | if (!decodedToken) { 82 | return done(new JsonWebTokenError('invalid token')); 83 | } 84 | 85 | const header = decodedToken.header; 86 | let getSecret; 87 | 88 | if(typeof secretOrPublicKey === 'function') { 89 | if(!callback) { 90 | return done(new JsonWebTokenError('verify must be called asynchronous if secret or public key is provided as a callback')); 91 | } 92 | 93 | getSecret = secretOrPublicKey; 94 | } 95 | else { 96 | getSecret = function(header, secretCallback) { 97 | return secretCallback(null, secretOrPublicKey); 98 | }; 99 | } 100 | 101 | return getSecret(header, function(err, secretOrPublicKey) { 102 | if(err) { 103 | return done(new JsonWebTokenError('error in secret or public key callback: ' + err.message)); 104 | } 105 | 106 | const hasSignature = parts[2].trim() !== ''; 107 | 108 | if (!hasSignature && secretOrPublicKey){ 109 | return done(new JsonWebTokenError('jwt signature is required')); 110 | } 111 | 112 | if (hasSignature && !secretOrPublicKey) { 113 | return done(new JsonWebTokenError('secret or public key must be provided')); 114 | } 115 | 116 | if (!hasSignature && !options.algorithms) { 117 | return done(new JsonWebTokenError('please specify "none" in "algorithms" to verify unsigned tokens')); 118 | } 119 | 120 | if (secretOrPublicKey != null && !(secretOrPublicKey instanceof KeyObject)) { 121 | try { 122 | secretOrPublicKey = createPublicKey(secretOrPublicKey); 123 | } catch (_) { 124 | try { 125 | secretOrPublicKey = createSecretKey(typeof secretOrPublicKey === 'string' ? Buffer.from(secretOrPublicKey) : secretOrPublicKey); 126 | } catch (_) { 127 | return done(new JsonWebTokenError('secretOrPublicKey is not valid key material')) 128 | } 129 | } 130 | } 131 | 132 | if (!options.algorithms) { 133 | if (secretOrPublicKey.type === 'secret') { 134 | options.algorithms = HS_ALGS; 135 | } else if (['rsa', 'rsa-pss'].includes(secretOrPublicKey.asymmetricKeyType)) { 136 | options.algorithms = RSA_KEY_ALGS 137 | } else if (secretOrPublicKey.asymmetricKeyType === 'ec') { 138 | options.algorithms = EC_KEY_ALGS 139 | } else { 140 | options.algorithms = PUB_KEY_ALGS 141 | } 142 | } 143 | 144 | if (options.algorithms.indexOf(decodedToken.header.alg) === -1) { 145 | return done(new JsonWebTokenError('invalid algorithm')); 146 | } 147 | 148 | if (header.alg.startsWith('HS') && secretOrPublicKey.type !== 'secret') { 149 | return done(new JsonWebTokenError((`secretOrPublicKey must be a symmetric key when using ${header.alg}`))) 150 | } else if (/^(?:RS|PS|ES)/.test(header.alg) && secretOrPublicKey.type !== 'public') { 151 | return done(new JsonWebTokenError((`secretOrPublicKey must be an asymmetric key when using ${header.alg}`))) 152 | } 153 | 154 | if (!options.allowInvalidAsymmetricKeyTypes) { 155 | try { 156 | validateAsymmetricKey(header.alg, secretOrPublicKey); 157 | } catch (e) { 158 | return done(e); 159 | } 160 | } 161 | 162 | let valid; 163 | 164 | try { 165 | valid = jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey); 166 | } catch (e) { 167 | return done(e); 168 | } 169 | 170 | if (!valid) { 171 | return done(new JsonWebTokenError('invalid signature')); 172 | } 173 | 174 | const payload = decodedToken.payload; 175 | 176 | if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) { 177 | if (typeof payload.nbf !== 'number') { 178 | return done(new JsonWebTokenError('invalid nbf value')); 179 | } 180 | if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) { 181 | return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000))); 182 | } 183 | } 184 | 185 | if (typeof payload.exp !== 'undefined' && !options.ignoreExpiration) { 186 | if (typeof payload.exp !== 'number') { 187 | return done(new JsonWebTokenError('invalid exp value')); 188 | } 189 | if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) { 190 | return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000))); 191 | } 192 | } 193 | 194 | if (options.audience) { 195 | const audiences = Array.isArray(options.audience) ? options.audience : [options.audience]; 196 | const target = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; 197 | 198 | const match = target.some(function (targetAudience) { 199 | return audiences.some(function (audience) { 200 | return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience; 201 | }); 202 | }); 203 | 204 | if (!match) { 205 | return done(new JsonWebTokenError('jwt audience invalid. expected: ' + audiences.join(' or '))); 206 | } 207 | } 208 | 209 | if (options.issuer) { 210 | const invalid_issuer = 211 | (typeof options.issuer === 'string' && payload.iss !== options.issuer) || 212 | (Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1); 213 | 214 | if (invalid_issuer) { 215 | return done(new JsonWebTokenError('jwt issuer invalid. expected: ' + options.issuer)); 216 | } 217 | } 218 | 219 | if (options.subject) { 220 | if (payload.sub !== options.subject) { 221 | return done(new JsonWebTokenError('jwt subject invalid. expected: ' + options.subject)); 222 | } 223 | } 224 | 225 | if (options.jwtid) { 226 | if (payload.jti !== options.jwtid) { 227 | return done(new JsonWebTokenError('jwt jwtid invalid. expected: ' + options.jwtid)); 228 | } 229 | } 230 | 231 | if (options.nonce) { 232 | if (payload.nonce !== options.nonce) { 233 | return done(new JsonWebTokenError('jwt nonce invalid. expected: ' + options.nonce)); 234 | } 235 | } 236 | 237 | if (options.maxAge) { 238 | if (typeof payload.iat !== 'number') { 239 | return done(new JsonWebTokenError('iat required when maxAge is specified')); 240 | } 241 | 242 | const maxAgeTimestamp = timespan(options.maxAge, payload.iat); 243 | if (typeof maxAgeTimestamp === 'undefined') { 244 | return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); 245 | } 246 | if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) { 247 | return done(new TokenExpiredError('maxAge exceeded', new Date(maxAgeTimestamp * 1000))); 248 | } 249 | } 250 | 251 | if (options.complete === true) { 252 | const signature = decodedToken.signature; 253 | 254 | return done(null, { 255 | header: header, 256 | payload: payload, 257 | signature: signature 258 | }); 259 | } 260 | 261 | return done(null, payload); 262 | }); 263 | }; 264 | --------------------------------------------------------------------------------