node-webtokens

Simple, opinionated implementation of JWS and JWE compact serialization

Usage no npm install needed!

<script type="module">
  import nodeWebtokens from 'https://cdn.skypack.dev/node-webtokens';
</script>

README

node-webtokens

Simple, opinionated implementation of JWS and JWE compact serialization.

Simple

All functions exposed through a single set of straightforward APIs.

const jwt = require('node-webtokens');

// JWS EXAMPLE
token = jwt.generate(alg, payload, key);
parsedToken = jwt.parse(token).verify(key);

// JWE EXAMPLE
token = jwt.generate(alg, enc, payload, key);
parsedToken = jwt.parse(token).verify(key);

Token parsing and token verification/decryption supported through chainable methods. When necessary, this enables the user to inspect the token header before proceeding with the verification/decryption. Here is an example:

parsedToken = jwt.parse(token);

if (parsedToken.error) {
  // error handling logic
} else {
  // inspect parsedToken.header

  // proceed with verification
  parsedToken.verify(key);
}

Token verification can be fine-tuned through additional chainable methods. Example:

parsedToken = jwt.parse(token)
                 .setTokenLifetime(120000)
                 .setAlgorithmList(['RS256', 'RS384'])
                 .setIssuer(['auth.mydomain.com'])
                 .setAudience(['A1B2C3D4E5.com.mydomain.myservice'])
                 .verify(key);

Keys can be automatically managed out of keystores (JavaScript objects holding multiple keys). Example:

keystore = {
  'e5739df2261c8a0ed41715e7f62cc295': 'SATKcp7AMnCg0YdEBPIcgknBplYttePtQoRddpJjyVak9F5vEp/7pL0Q1236MkVQd7nIXGoaPt4w1dlrpEmY4A==',
  'f0fd89c4abe83811ee9afa92d0d687f7': '6Bzisgmhj9LGJDNjx/WBNRUsnZA8pXRpVxB7Pf8ar29XI158V4+t1GEqkCl5MYZhcOMTi5fa3yYr0Vcya6vUkA==',
  '20e009a52cd91dc7dc7a8d7da525fed5': '+PC/htwSB6pz4VRTcGL1iN74xlqoX6Q2oilsraVvSVefL+lr0tW1+/pOGQpdZpXtN20DjfbC0s4rHYZD2z924Q=='
};

token = jwt.generate(alg, payload, keystore, kid);
parsedToken = jwt.parse(token).verify(keystore);

Opinionated

There are various npm packages that cover the IETF JOSE scope striving for generality and flexibility. This specific package is shaped after the following strong assumptions, which somehow restrict its usability:

  • No effort to ensure compatibility with older Node.js versions. Most stringent requirement comes from the use of crypto.timingSafeEqual(), which is not available in Node.js versions prior to v6.6.0;
  • The JWS/JWE payload must be a JavaScript object (a.k.a. hash or dictionary);
  • The iat claim is automatically added to the payload at token generation time, and comes in the form of a Unix timestamp (number of seconds);
  • The JWS/JWE header is automatically generated at token generation time, with limited control by the user.

Installation

npm install node-webtokens --save

Supported JWS algorithms

Algorithm Minimum key requirements
HS256 32-octet key, passed either as base64 string or as buffer; same key for token generation and token verification
HS384 48-octet key, passed either as base64 string or as buffer; same key for token generation and token verification
HS512 64-octet key, passed either as base64 string or as buffer; same key for token generation and token verification
RS256 2048-bit RSA key in PEM format, passed either as UTF-8 string or as buffer; private key for token generation, public key or certificate for token verification
RS384 2048-bit RSA key in PEM format, passed either as UTF-8 string or as buffer; private key for token generation, public key or certificate for token verification
RS512 2048-bit RSA key in PEM format, passed either as UTF-8 string or as buffer; private key for token generation, public key or certificate for token verification
ES256 P-256 EC key in PEM format, passed either as UTF-8 string or as buffer; private key for token generation, public key or certificate for token verification; P-256 keys are identified as prime256v1 in OpenSSL
ES384 P-384 EC key in PEM format, passed either as UTF-8 string or as buffer; private key for token generation, public key or certificate for token verification; P-384 keys are identified as secp384r1 in OpenSSL
ES512 P-521 EC key in PEM format, passed either as UTF-8 string or as buffer; private key for token generation, public key or certificate for token verification; P-521 keys are identified as secp521r1 in OpenSSL

Table 1 - List of JWS algorithms

Supported JWE key management algorithms

Algorithm Minimum key requirements
RSA-OAEP 2048-bit RSA key in PEM format, passed either as UTF-8 string or as buffer; public key or certificate for token generation, private key for token decryption
A128KW 16-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A192KW 24-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A256KW 32-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
dir n/a
PBES2-HS256+A128KW Password, passed either as UTF-8 string or as buffer; same password for token generation and token decryption; a 16-octet key is derived from the password through PBKDF2
PBES2-HS384+A192KW Password, passed either as UTF-8 string or as buffer; same password for token generation and token decryption; a 24-octet key is derived from the password through PBKDF2
PBES2-HS512+A256KW Password, passed either as UTF-8 string or as buffer; same password for token generation and token decryption; a 32-octet key is derived from the password through PBKDF2

Table 2 - List of JWE key management algorithms

Supported JWE content encryption algorithms

Algorithm Minimum key requirements (*)
A128CBC-HS256 32-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A192CBC-HS384 48-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A256CBC-HS512 64-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A128GCM 16-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A192GCM 24-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption
A256GCM 32-octet key, passed either as base64 string or as buffer; same key for token generation and token decryption

Table 3 - List of JWE content encryption algorithms

(*) These requirements are relevant only when direct content encryption is used (key management algorithm equal to dir). In all the other cases, the JWE generation API takes care of generating a single-use content encryption key of appropriate length.

Synchronous vs. asynchronous

The token generation API and token verification API can both be used in either synchronous or asynchronous mode. Example:

// SYNCHRONOUS API MODE
token = jwt.generate('HS256', payload, key);
parsedToken = jwt.parse(token).verify(key);

// ASYNCHRONOUS API MODE
jwt.generate('PBES2-HS512+A256KW', 'A256GCM', payload, pwd, (error, token) => {
  jwt.parse(token).verify(pwd, (error, parsedToken) => {
    // other statements
  });
});

All the Node.js crypto functions used in this package are synchronous, with the exception of PBKDF2, which can be invoked either synchronously as crypto.pbkdf2Sync() or asynchronously as crypto.pbkdf2(). This implies that the use of the asynchronous API mode makes a real difference in terms of execution only when one of the algorithms based on PBKDF2 is selected, namely PBES2-HS256+A128KW, PBES2-HS384+A192KW or PBES2-HS512+A256KW.

Use of the token generation and token verification APIs in asynchronous mode is recommended for JWE when the selected key management algorithm is PBES2-HS256+A128KW, PBES2-HS384+A192KW or PBES2-HS512+A256KW. Conversely, use of the synchronous mode is preferable for JWS and for all other JWE cases.

Token generation

Single API, supporting two slightly different usage patterns, each with synchronous and asynchronous mode:

jwt.generate(alg, [enc,] payload, key[, callback])
jwt.generate(alg, [enc,] payload, keystore, kid[, callback])

  • alg - String corresponding to one of the algorithms listed in Table 1 for JWS or in Table 2 for JWE (case sensitive spelling);
  • enc - Present only for JWE; string corresponding to one of the algorithms listed in Table 3 (case sensitive spelling);
  • payload - JavaScript object (a.k.a. hash or dictionary); if already present, the iat claim is overridden at token generation time;
  • key - Key subject to the requirements specified in Table 1, Table 2 or Table 3, depending on the selected alg value;
  • keystore - JavaScript object holding multiple keys;
  • kid - Key identifier; string; must exist in keystore.

When the keystore / kid pattern is used, the kid claim is automatically added to the token header.

When used in synchronous mode, the token generation API returns the token as string. When used in asynchronous mode, the callback function is invoked with parameters (error, token).

Token parsing and verification/decryption

Token parsing and token verification/decryption are supported through chainable methods.

jwt.parse(token)

The token parsing API is invoked with the token (string) as input and returns a ParsedToken object with the following properties:

  • error - Error condition as JavaScript object; present if parsing could not be completed (e.g., invalid token, non parsable header);
  • parts - Array of strings, with each string corresponding to one of the token parts (three parts for JWS, five parts for JWE);
  • type - Either JWS or JWE or not present, with the latter relevant in case the token type could not be recognized (invalid token error);
  • header - Token header as JavaScript object; not present if the token header could not be parsed;
  • payload - Token payload as JavaScript object; present only for JWS tokens and in absence of errors; for JWE tokens the payload gets added to the ParsedToken object after decryption, which is performed when the token verification API is invoked.

Token parsing never throws errors. Any error condition encountered during parsing is reported in the error object.

parsedToken.setTokenLifetime(lifetime)

The setTokenLifetime method can be used to configure the token lifetime to be considered when assessing the token validity. The parameter lifetime constraints the maximum number of seconds elapsed since the generation of the token (indicated by the iat claim in the token payload).

If the setTokenLifetime method is not used, then token verification does not encompass expiration based on the iat claim. However, if the token payload contains the exp claim, then the token is still subject to expiration based on the exp claim.

The setTokenLifetime method does not throw errors. The specified lifetime value is simply ignored if it is not an integer number greater than zero.

Token verification does not enforce the presence of the exp claim in the token payload. However, if present, the exp claim is processed.

parsedToken.setAlgorithmList(algList[, encList])

The setAlgorithmList method can be used to configure the list of algorithms that are considered acceptable:

  • algList - String or array of strings corresponding to one or multiple of the algorithms listed in Table 1 for JWS or in Table 2 for JWE (case-sensitive spelling);
  • encList - Only relevant for JWE; string or array of strings corresponding to one or multiple of the algorithms listed in Table 3 (case-sensitive spelling);

Integrity check/decryption is not attempted during verification if the token under verification does not comply with the configured algorithm list. In that case, the token is simply reported as invalid because of the unwanted algorithm.

The setAlgorithmList method does not throw errors. If the algorithm list contains only invalid or non-existent algorithms, then all the tokens are reported as invalid.

parsedToken.setAudience(audList)

The setAudience method can be used to configure the acceptable values of the aud claim. Input parameter audList can be a string or an array of strings.

The setAudience method does not throw errors. If audList is not a string or an array of strings, then the action is simply ignored.

Token verification enforces the presence of the aud claim in the token payload only if the setAudience method is invoked before proceeding with the verification.

parsedToken.setIssuer(issList)

The setIssuer method can be used to configure the acceptable values of the iss claim. Input parameter issList can be a string or an array of strings.

The setIssuer method does not throw errors. If issList is not a string or an array of strings, then the action is simply ignored.

Token verification enforces the presence of the iss claim in the token payload only if the setIssuer method is invoked before proceeding with the verification.

parsedToken.verify(key[, callback])
parsedToken.verify(keystore[, callback])

  • key - Key subject to the requirements specified in Table 1, Table 2 or Table 3, depending on the alg claim found in the token header;
  • keystore - JavaScript object holding multiple keys; the key used for verification is determined on the basis of the kid claim found in the token header.

When used in synchronous mode, the verify method returns the ParsedToken object enriched with additional properties. When used in asynchronous mode, the callback function is invoked with parameters (error, parsedToken).

After the verification, the ParsedToken object exposes the following properties:

  • valid - Present and equal to true (boolean) if the token is valid and not expired; absent in all other cases;
  • expired - Present and equal to the token expiration time (Unix timestamp, seconds) if the token is valid but expired; absent in all other cases;
  • error - Error condition as JavaScript object; present if token verification could not be completed or the token was found invalid; absent in all other cases; when present, the error object always includes the message property that specifies the reason why token verification failed;
  • parts - Array of strings, with each string corresponding to one of the token parts (three parts for JWS, five parts for JWE);
  • type - Either JWS or JWE; always present for valid or expired tokens; may not be present otherwise;
  • header - Token header as JavaScript object; always present for valid or expired tokens; may not be present otherwise;
  • payload - Token payload as JavaScript object; always present for valid or expired tokens; may not be present otherwise.

The following example illustrates a plausible handling of the final ParsedToken object:

if (parsedToken.error) {
  // error handling; parsedToken.error.message provides details

} else if (parsedToken.expired) {
  // token has expired; the parsedToken.expired value indicates when

} else {
  // token is valid; parsedToken.payload is ready for use

}

Examples

Token generated/verified with individual key:

const jwt = require('node-webtokens');

var key = getKeyFromSomewhere();

var payload = {
  iss: 'auth.mydomain.com',
  aud: 'A1B2C3D4E5.com.mydomain.myservice',
  sub: 'jack.sparrow@example.com',
  info: 'Hello World!',
  list: [1, 2, 3]
};

var token = jwt.generate('HS512', payload, key);
console.log(token);
// eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhdXRoLm15ZG9tYWluLmNvbSIsImF1ZCI6IkExQjJDM0Q0RTUuY29tLm15ZG9tYWluLm15c2VydmljZSIsInN1YiI6ImphY2suc3BhcnJvd0BleGFtcGxlLmNvbSIsImluZm8iOiJIZWxsbyBXb3JsZCEiLCJsaXN0IjpbMSwyLDNdLCJpYXQiOjE0OTQ0NTEwMDR9.Rzb8KJ6du4QKnd9goevhswj56Y3polY_IwF6_onDKxa9IbEtBCUBfmgdZDZdE5meLBUFw9PaMqbj3fo3L3JEQA

var parsed = jwt.parse(token).verify(key);
console.log(parsed.valid);
// true
console.log(parsed.header);
// { alg: 'HS512' }
console.log(parsed.payload);
/* { iss: 'auth.mydomain.com',
     aud: 'A1B2C3D4E5.com.mydomain.myservice',
     sub: 'jack.sparrow@example.com',
     info: 'Hello World!',
     list: [ 1, 2, 3 ],
     iat: 1494451004 } */

Token generated/verified with keystore:

var keystore = {
  'e5739df2261c8a0ed41715e7f62cc295': 'SATKcp7AMnCg0YdEBPIcgknBplYttePtQoRddpJjyVak9F5vEp/7pL0Q1236MkVQd7nIXGoaPt4w1dlrpEmY4A==',
  'f0fd89c4abe83811ee9afa92d0d687f7': '6Bzisgmhj9LGJDNjx/WBNRUsnZA8pXRpVxB7Pf8ar29XI158V4+t1GEqkCl5MYZhcOMTi5fa3yYr0Vcya6vUkA==',
  '20e009a52cd91dc7dc7a8d7da525fed5': '+PC/htwSB6pz4VRTcGL1iN74xlqoX6Q2oilsraVvSVefL+lr0tW1+/pOGQpdZpXtN20DjfbC0s4rHYZD2z924Q=='
};

token = jwt.generate('HS512', payload, keystore, 'f0fd89c4abe83811ee9afa92d0d687f7');
console.log(token);
// eyJhbGciOiJIUzUxMiIsImtpZCI6ImYwZmQ4OWM0YWJlODM4MTFlZTlhZmE5MmQwZDY4N2Y3In0.eyJpc3MiOiJhdXRoLm15ZG9tYWluLmNvbSIsImF1ZCI6IkExQjJDM0Q0RTUuY29tLm15ZG9tYWluLm15c2VydmljZSIsInN1YiI6ImphY2suc3BhcnJvd0BleGFtcGxlLmNvbSIsImluZm8iOiJIZWxsbyBXb3JsZCEiLCJsaXN0IjpbMSwyLDNdLCJpYXQiOjE0OTQ0NTEwMDR9.z9mawWuGjE0eIQV08YtWTrlD7OAnmxGaLWFiBlXMn9MwzYHE-Sa9KhPLeeWuSx1c8at62F2IegK8O61gDGUA_g

parsed = jwt.parse(token).verify(keystore);
console.log(parsed.valid);
// true
console.log(parsed.header);
// { alg: 'HS512', kid: 'f0fd89c4abe83811ee9afa92d0d687f7' }

Note the kid claim automatically added to the token header.

Verification key not found in keystore:

token = jwt.generate('HS512', payload, keystore, 'f0fd89c4abe83811ee9afa92d0d687f7');

delete keystore['f0fd89c4abe83811ee9afa92d0d687f7'];

parsed = jwt.parse(token).verify(keystore);
console.log(parsed.error);
/* { message: 'Key with id not found',
     kid: 'f0fd89c4abe83811ee9afa92d0d687f7' } */

The offending key identifier is exposed in the error object.

Expired token:

token = jwt.generate('HS512', payload, key);

setTimeout(() => {
  parsed = jwt.parse(token).setTokenLifetime(3).verify(key);
  console.log(parsed.expired);
  // 1494451007

}, 5000);

In the above example, expiration is determined on the basis of the iat claim and of the configured token lifetime (3 seconds). However, in case the token payload contains the exp claim, that is considered as well.

Token using unwanted algorithm:

token = jwt.generate('HS256', payload, key);

parsed = jwt.parse(token)
            .setAlgorithmList(['HS384', 'HS512'])
            .verify(key);

console.log(parsed.error);
// { message: 'Unwanted algorithm HS256' }

Parsing and verification as separate steps. JWS example:

token = jwt.generate('HS512', payload, key);

parsed = jwt.parse(token);
console.log(parsed.header);
// { alg: 'HS512' }
console.log(parsed.payload);
/* { iss: 'auth.mydomain.com',
     aud: 'A1B2C3D4E5.com.mydomain.myservice',
     sub: 'jack.sparrow@example.com',
     info: 'Hello World!',
     list: [ 1, 2, 3 ],
     iat: 1494451807 } */

parsed.setTokenLifetime(600).verify(key);
console.log(parsed.valid);
// true

Parsing and verification as separate steps. JWE example:

token = jwt.generate('A256KW', 'A256GCM', payload, key);

parsed = jwt.parse(token);
console.log(parsed.header);
// { alg: 'A256KW', enc: 'A256GCM' }
console.log(parsed.payload);
// undefined

parsed.setTokenLifetime(600).verify(key);
console.log(parsed.valid);
// true
console.log(parsed.payload);
/* { iss: 'auth.mydomain.com',
     aud: 'A1B2C3D4E5.com.mydomain.myservice',
     sub: 'jack.sparrow@example.com',
     info: 'Hello World!',
     list: [ 1, 2, 3 ],
     iat: 1494451807 } */

Token generation with asynchronous API:

jwt.generate('PBES2-HS512+A256KW', 'A256GCM', payload, key, (error, token) => {
  console.log(token);
  // eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjoxMDAwLCJwMnMiOiJVRUpGVXpJdFNGTTFNVElyUVRJMU5rdFhBRGN0N2dnMk1qWGsifQ.IeIzzbZBtb65xF9z1I_L39up0V7FBtSlTJKMNft4_DD6pdQEiIMXAw.kihjXwJhu2ZC3ckd.CTbf_iRZrYho2Y-iw-1IHVh-POCYzBX0QhZ3j3onycb3hjMU6iWKokiKKeyzyG8UGLKO8uT5pyndGUNmyGAc-sJSMwZN5chHovet2JRsxjDC4PWiaMoE423eMqI3cc3iK4k9c71aKOOQOsEXbBohKwOy-nnlwU62ombiRejptb5p22V-FL7OwqK14-EcKSJxnvU8XRq4pX9HWU9G.jMnFV6OK2yBVUnw-W7YJKA
});

With PBES2, key derivation at token generation time performs 1024 PBKDF2 iterations. Hence the recommendation to use the asynchronous mode.

Token verification with asynchronous API:

jwt.parse(token).setTokenLifetime(600).verify(key, (error, parsed) => {
    console.log(parsed.valid);
    // true
    console.log(parsed.header);
    /* { alg: 'PBES2-HS512+A256KW',
         enc: 'A256GCM',
         p2c: 1024,
         p2s: 'UEJFUzItSFM1MTIrQTI1NktXADct7gg2MjXk' } */
    console.log(parsed.payload);
    /* { iss: 'auth.mydomain.com',
         aud: 'A1B2C3D4E5.com.mydomain.myservice',
         sub: 'jack.sparrow@example.com',
         info: 'Hello World!',
         list: [ 1, 2, 3 ],
         iat: 1494451023 } */
  });

With PBES2, key derivation at token verification time performs the number of PBKDF2 iterations indicated by the p2c claim in the JWE header. For protection against bogus tokens, the token verification API rejects p2c values larger than 1024 when used in synchronous mode or 16384 when used in asynchronous mode.

Credits

The JavaScript code used for ECDSA signature conversion from DER to concatenated and vice-versa is directly derived from the ecdsa-sig-formatter module.