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');
token = jwt.generate(alg, payload, key);
parsedToken = jwt.parse(token).verify(key);
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) {
} else {
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 packages in npm 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 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:
token = jwt.generate('HS256', payload, key);
parsedToken = jwt.parse(token).verify(key);
jwt.generate('PBES2-HS512+A256KW', 'A256GCM', payload, pwd, (error, token) => {
jwt.parse(token).verify(pwd, (error, parsedToken) => {
});
});
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 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
.
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 teh 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 teh 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) {
} else if (parsedToken.expired) {
} else {
}
Examples
Token generated/verified with individual key:
const jwt = require('../index.js');
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);
var parsed = jwt.parse(token).verify(key);
console.log(parsed.valid);
console.log(parsed.header);
console.log(parsed.payload);
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);
parsed = jwt.parse(token).verify(keystore);
console.log(parsed.valid);
console.log(parsed.header);
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);
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);
}, 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);
Parsing and verification as separate steps. JWS example:
token = jwt.generate('HS512', payload, key);
parsed = jwt.parse(token);
console.log(parsed.header);
console.log(parsed.payload);
parsed.setTokenLifetime(600).verify(key);
console.log(parsed.valid);
Parsing and verification as separate steps. JWE example:
token = jwt.generate('A256KW', 'A256GCM', payload, key);
parsed = jwt.parse(token);
console.log(parsed.header);
console.log(parsed.payload);
parsed.setTokenLifetime(600).verify(key);
console.log(parsed.valid);
console.log(parsed.payload);
Token generation with asynchronous API:
jwt.generate('PBES2-HS512+A256KW', 'A256GCM', payload, key, (error, token) => {
console.log(token);
});
With PBES2, key derivation at token generation time performs 1000 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);
console.log(parsed.header);
console.log(parsed.payload);
});
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 1000 when used in synchronous mode or 10000 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.