Comparing version 0.3.1 to 1.0.0
196
index.js
@@ -1,2 +0,1 @@ | ||
const crypto = require('crypto'); | ||
const path = require('path'); | ||
@@ -14,3 +13,3 @@ const exec = require('child_process').exec; | ||
const CLIENT_CACHE_SIZE_MAX = 50; | ||
const CLIENT_CACHE_SIZE_MAX = 200; | ||
const clientCache = new Qlru({maxSize : CLIENT_CACHE_SIZE_MAX}); | ||
@@ -24,2 +23,4 @@ // how many time before expiration do we renew the token in millisecond | ||
const TOKEN_COOKIE_REGEXP = /access_token\s*=([^;]+?)(?:;|$)/; | ||
/** | ||
@@ -124,2 +125,4 @@ * Decode base64 url if there is in the token | ||
let _tokenString = _headerBase64 + '.' + _payloadBase64; | ||
let _header = null; | ||
let _payload = null; | ||
@@ -129,29 +132,29 @@ try { | ||
let _payloadString = base64urlDecode(_payloadBase64); | ||
let _header = JSON.parse(_headerString); | ||
let _payload = JSON.parse(_payloadString); | ||
_header = JSON.parse(_headerString); | ||
_payload = JSON.parse(_payloadString); | ||
} | ||
catch (e) { | ||
return callback(new Error('Invalid JSON Web Token: ' + e.message)); | ||
} | ||
if (!(_payload instanceof Object)) { | ||
return callback(new Error('Invalid Payload JSON Web token. It is not an object')); | ||
} | ||
if (!(_payload instanceof Object)) { | ||
return callback(new Error('Invalid Payload JSON Web token. It is not an object')); | ||
} | ||
if (!(_header instanceof Object)) { | ||
return callback(new Error('Invalid Header JSON Web Token. It is not an object')); | ||
} | ||
if (!(_header instanceof Object)) { | ||
return callback(new Error('Invalid Header JSON Web Token. It is not an object')); | ||
} | ||
if (_header.alg !== ALGORITHM_NAME) { | ||
return callback(new Error('Algorithm not accepted for JSON Web Token. Only ' + ALGORITHM_NAME + ' is accepted')); | ||
} | ||
if (_header.alg !== ALGORITHM_NAME) { | ||
return callback(new Error('Algorithm not accepted for JSON Web Token. Only ' + ALGORITHM_NAME + ' is accepted')); | ||
} | ||
if (_payload.exp && Date.now() > parseInt(_payload.exp, 10) * 1000) { | ||
return callback(new Error('JSON Web Token expired')); | ||
} | ||
if (_payload.exp && Date.now() > parseInt(_payload.exp, 10) * 1000) { | ||
return callback(new Error('JSON Web Token expired')); | ||
} | ||
if (_payload.iss === '' || _payload.iss === undefined || _payload.iss === null) { | ||
return callback(new Error('JSON Web Token without issuer')); | ||
} | ||
return callback(null, _payload, _tokenString, _signature); | ||
if (_payload.iss === '' || _payload.iss === undefined || _payload.iss === null) { | ||
return callback(new Error('JSON Web Token without issuer')); | ||
} | ||
catch (e) { | ||
return callback(new Error('Invalid JSON Web Token ' + e.toString())); | ||
} | ||
return callback(null, _payload, _tokenString, _signature); | ||
} | ||
@@ -179,3 +182,3 @@ | ||
} catch (e) { | ||
return callback(new Error('Invalid JSON Web Token' + e.toString())); | ||
return callback(new Error('Invalid JSON Web Token: ' + e.message)); | ||
} | ||
@@ -207,4 +210,5 @@ return callback(null, payload); | ||
* @param {String} privKey private key | ||
* @param {Object} data user data | ||
*/ | ||
function getToken (clientId, serverId, privKey) { | ||
function getToken (clientId, serverId, privKey, data) { | ||
let _cacheKey = clientId + '_' + serverId; | ||
@@ -217,3 +221,3 @@ | ||
} | ||
let _newToken = generate(clientId, serverId, DEFAULT_EXPIRE_IN, privKey); | ||
let _newToken = generate(clientId, serverId, DEFAULT_EXPIRE_IN, privKey, data); | ||
@@ -229,5 +233,67 @@ clientCache.set(_cacheKey, { | ||
/** | ||
* Verify token with a list of public keys | ||
* | ||
* @param {Array} publicKeys array of public keys | ||
* @param {Object} payload | ||
* @param {String} tokenString | ||
* @param {String} signature | ||
* @param {Function} callback(err) | ||
* @param {Number} i iterator of public keys | ||
*/ | ||
function verifyTokenForEachPublicKey (publicKeys, payload, tokenString, signature, callback, i = 0) { | ||
verifyToken(payload, tokenString, signature, publicKeys[i], (err) => { | ||
i++; | ||
if (!err || i >= publicKeys.length) { | ||
return callback(err); | ||
} | ||
// avoid looping without releasing NodeJS event loop | ||
process.nextTick(() => { | ||
verifyTokenForEachPublicKey(publicKeys, payload, tokenString, signature, callback, i); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Check if a token exists in Authorization header first or in cookies | ||
* | ||
* @param {Object} req Req from request | ||
* @param {Function} callback(err, oken) | ||
*/ | ||
function findToken (req, callback) { | ||
if (!req.headers) { | ||
return callback(new Error('JSON Web Token - No HTTP header detected')); | ||
} | ||
// Accept tokens in authorization header and cookies | ||
let _token = req.headers.authorization || req.headers.Authorization || parseCookie(req.headers.cookie); | ||
if (typeof _token !== 'string' || _token.length === 0) { | ||
return callback(new Error('No JSON Web Token detected in Authorization header or Cookie. Format is "Authorization: jwt" or "Cookie: access_token=jwt"')); | ||
} | ||
// remove Bearer keyword if present | ||
if (/^Bearer /i.test(_token) === true) { | ||
return callback(null, _token.slice(7)); | ||
} | ||
return callback(null, _token); | ||
} | ||
/** | ||
* Parse cookie to get JWT in access_token key | ||
* | ||
* @param {String} cookie req.headers.cookie | ||
* @return {String} jwt if found, null otherwise | ||
*/ | ||
function parseCookie (cookie) { | ||
var _token = TOKEN_COOKIE_REGEXP.exec(cookie); | ||
if (_token instanceof Array && _token.length > 1) { | ||
return _token[1].trim(); | ||
} | ||
return null; | ||
} | ||
/** | ||
* Generate a middleware for Express | ||
* | ||
* | ||
* @param {Mixed} serverId accepted server id | ||
@@ -239,46 +305,43 @@ * @param {String} getPublicKeyFn public key of client | ||
return function (req, res, next) { | ||
if (!req.headers) { | ||
return next(new Error('JSON Web Token - No HTTP header detected')); | ||
} | ||
let _auth = req.headers.Authorization || req.headers.authorization; | ||
if (typeof _auth !== 'string') { | ||
return next(new Error('No Authorization HTTP header detected. Format is "Authorization: Bearer jwt"')); | ||
} | ||
if (/^Bearer /i.test(_auth) === false) { | ||
return next(new Error('No Bearer JSON Web Token detected. Format is "Authorization: Bearer jwt"')); | ||
} | ||
let _token = _auth.slice(7); | ||
// is it in cache, fast return of errors or not | ||
let _cachedToken = serverCache.get(_token); | ||
if (_cachedToken) { | ||
if (_cachedToken.payload !== undefined && _cachedToken.payload.exp !== undefined && Date.now() > parseInt(_cachedToken.payload.exp, 10) * 1000) { | ||
_cachedToken.err = new Error('JSON Web Token expired'); | ||
} | ||
if (_cachedToken.err) { | ||
return next(_cachedToken.err); | ||
} | ||
req.jwtPayload = _cachedToken.payload; | ||
return next(); | ||
} | ||
// otherwise, compute everything | ||
parseToken(_token, (err, payload, tokenString, signature) => { | ||
// Get token in authorization header or cookies | ||
findToken(req, (err, _token) => { | ||
if (err) { | ||
serverCache.set(_token, { payload : payload, err : err }); | ||
return next(err); | ||
} | ||
if (payload && payload.aud !== serverId) { | ||
let _err = new Error('Invalid JSON Web Token audience'); | ||
serverCache.set(_token, { payload : payload, err : _err }); | ||
return next(_err); | ||
// is it in cache, fast return of errors or not | ||
let _cachedToken = serverCache.get(_token); | ||
if (_cachedToken) { | ||
if (_cachedToken.payload !== undefined && _cachedToken.payload.exp !== undefined && Date.now() > parseInt(_cachedToken.payload.exp, 10) * 1000) { | ||
_cachedToken.err = new Error('JSON Web Token expired'); | ||
} | ||
if (_cachedToken.err) { | ||
return next(_cachedToken.err); | ||
} | ||
req.jwtPayload = _cachedToken.payload; | ||
return next(); | ||
} | ||
getPublicKeyFn(req, res, payload, function (publicKey) { | ||
verifyToken(payload, tokenString, signature, publicKey, (err) => { | ||
// otherwise, compute everything | ||
parseToken(_token, (err, payload, tokenString, signature) => { | ||
if (err) { | ||
serverCache.set(_token, { payload : payload, err : err }); | ||
if (err) { | ||
return next(err); | ||
} | ||
req.jwtPayload = payload; | ||
next(); | ||
return next(err); | ||
} | ||
if (payload && payload.aud !== serverId) { | ||
let _err = new Error('Invalid JSON Web Token audience'); | ||
serverCache.set(_token, { payload : payload, err : _err }); | ||
return next(_err); | ||
} | ||
getPublicKeyFn(req, res, payload, function (publicKey) { | ||
var _publicKeys = (publicKey instanceof Array) ? publicKey : [publicKey]; | ||
verifyTokenForEachPublicKey(_publicKeys, payload, tokenString, signature, (err) => { | ||
// what ever happens, put result in cache | ||
serverCache.set(_token, { payload : payload, err : err }); | ||
if (err) { | ||
return next(err); | ||
} | ||
req.jwtPayload = payload; | ||
next(); | ||
}); | ||
}); | ||
@@ -306,4 +369,5 @@ }); | ||
generateECDHKeys, | ||
parseCookie, | ||
generateAuto : getToken // deprecated | ||
}; | ||
{ | ||
"name": "kitten-jwt", | ||
"version": "0.3.1", | ||
"version": "1.0.0", | ||
"description": "Keep It Simple, Stupid, Secure and Fast JWT module", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "./node_modules/mocha/bin/mocha -w" | ||
"test": "./node_modules/mocha/bin/mocha -w -b -t 5000" | ||
}, | ||
@@ -31,6 +31,3 @@ "repository": { | ||
"quick-lru": "2.0.0" | ||
}, | ||
"publishConfig": { | ||
"registry": "https://registry.npmjs.org/" | ||
} | ||
} |
@@ -5,4 +5,2 @@ # Kitten JWT | ||
Maintained by https://www.easilys.com and https://carbone.io | ||
## Philosophy and why | ||
@@ -21,2 +19,4 @@ | ||
This module solves this for you. It chooses a highly secured algorithm by default. If you want another algorithm, fork it. | ||
The algorithm used (asymmetric) allow the client to generate himself a token without having to exchange a secret with the server. | ||
Only the public key is exchanged. | ||
@@ -45,4 +45,6 @@ To save extra bandwidth, it let you define only two parameters : a client id ("Alice", issuer), and a server id ("Bob", audience). | ||
* On client-side | ||
#### 1) On client-side | ||
Using `request` module for example: | ||
```js | ||
@@ -53,12 +55,21 @@ var jwt = require('kitten-jwt'); | ||
// This function is very fast (uses cache), it can be called for every HTTP request | ||
var header = jwt.getToken('client-id-1220202', 'server-app-name', 'privKeyOfTheClient'); | ||
var token = jwt.getToken('client-id-1220202', 'server-app-name', 'privKeyOfTheClient'); | ||
// Insert the token in HTTP Header, it will be parsed by jwt.verifyHTTPHeaderFn automatically | ||
request.setHeader('Authorization', 'Bearer ' + header); | ||
request.setHeader('Authorization', 'Bearer ' + token); // "Bearer" keyword is optional | ||
``` | ||
* On server-side | ||
Or, if your client is a browser, store the JWT in a `cookie` instead of `Authorization` header. | ||
With `ExpressJS`: | ||
```js | ||
// let the browser send it back automatically. | ||
// Do not forget to refresh it before the 12-hour expiration | ||
response.cookie('access_token', token); | ||
``` | ||
#### 2) On server-side | ||
```js | ||
var jwt = require('kitten-jwt'); | ||
@@ -69,3 +80,4 @@ | ||
var _clientId = payload.iss; | ||
// do whatever you want: db query, file read | ||
// do whatever you want: db query, file read to return the public key | ||
// it accepts an array of public key ['pubKeyOfTheClient1', 'pubKeyOfTheClient2'] | ||
return callback('pubKeyOfTheClient'); | ||
@@ -76,2 +88,3 @@ } | ||
// This function is very fast (uses lru-cache) | ||
// It searches JWT in req.header.authorization, then in req.header.cookie.<access_token> | ||
express().use(jwt.verifyHTTPHeaderFn('server-app-name', getPublicKeyFn)); | ||
@@ -130,6 +143,7 @@ | ||
Generate a function(req, req, next)<br> | ||
Set req.jwtPayload | ||
Generate a middleware `function(req, req, next)`<br> | ||
Verify and set `req.jwtPayload` | ||
- getPublicKeyFn : Function(req, res, payload, callback) which returns publicKey in callback(pubKey) | ||
- getPublicKeyFn : Function(req, res, payload, callback) which must call the `callback(String|Array)` where | ||
the parameter is either a string (one public key) or an array of strings (mutliple public key to test) | ||
- serverId : JWT audience, token.aud | ||
@@ -164,2 +178,14 @@ if the token is invalid, next(err) is called. Thus you can catch the error in another 4-parameter middlewares. | ||
## CHANGELOG | ||
**1.0.0** | ||
- Possibility to verify a token with multiple public keys. `getPublicKeyFn` can return an array of public keys | ||
- Increase key cache to 200 | ||
- Improve error output | ||
- Accepts token in cookie `access_token` | ||
- Accepts token without key word "Bearer" | ||
## Notes | ||
@@ -169,3 +195,2 @@ | ||
- blacklist IP which send too many bad token | ||
- to save extra bandwithh: kitten-jwt accepts and generate tokens with one-letter header instead of RFCs JWT header (optional) | ||
@@ -177,3 +202,2 @@ - make expiration a little bit random | ||
https://github.com/volschin/node-curve25519 | ||
https://ianix.com/pub/curve25519-deployment.html | ||
https://ianix.com/pub/curve25519-deployment.html |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
29958
5
323
0
193