-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* inital commit * conform errors to match jwt's
- Loading branch information
Showing
8 changed files
with
429 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"root": true, | ||
"extends": ["warp/node", "warp/es6"], | ||
"parserOptions": { | ||
"ecmaVersion": 2017 | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": [ "tests/*.js" ], | ||
"env": { | ||
"jest": true | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,2 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
*.pid.lock | ||
|
||
# Directory for instrumented libs generated by jscoverage/JSCover | ||
lib-cov | ||
|
||
# Coverage directory used by tools like istanbul | ||
coverage | ||
|
||
# nyc test coverage | ||
.nyc_output | ||
|
||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | ||
.grunt | ||
|
||
# Bower dependency directory (https://bower.io/) | ||
bower_components | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# Compiled binary addons (http://nodejs.org/api/addons.html) | ||
build/Release | ||
|
||
# Dependency directories | ||
node_modules/ | ||
jspm_packages/ | ||
|
||
# Typescript v1 declaration files | ||
typings/ | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional eslint cache | ||
.eslintcache | ||
|
||
# Optional REPL history | ||
.node_repl_history | ||
|
||
# Output of 'npm pack' | ||
*.tgz | ||
|
||
# Yarn Integrity file | ||
.yarn-integrity | ||
|
||
# dotenv environment variables file | ||
.env | ||
|
||
/node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package-lock=false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
language: node_js | ||
node_js: | ||
- "8" | ||
after_success: ./node_modules/.bin/codecov |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,12 @@ | ||
# jwt-plus | ||
Opinionated JWT library with sane defaults. | ||
[![Build Status](https://travis-ci.com/wearereasonablepeople/jwt-plus.svg?token=yQTBKvDF8NXw5WvCpzqf&branch=master)](https://travis-ci.com/wearereasonablepeople/jwt-plus) | ||
[![codecov](https://codecov.io/gh/wearereasonablepeople/jwt-plus/branch/master/graph/badge.svg?token=tHRvIF5F3v)](https://codecov.io/gh/wearereasonablepeople/jwt-plus) | ||
|
||
|
||
## Description | ||
An opinionated JWT library with sensible defaults that implements the complete token flow. | ||
|
||
## Install | ||
``` | ||
npm install jwt-plus | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
'use strict'; | ||
|
||
const jsonwebtoken = require('jsonwebtoken'); | ||
const randToken = require('rand-token'); | ||
const generator = randToken.generator({source: 'crypto'}); | ||
const t = require('tcomb'); | ||
const {mergeAll, dissoc} = require('ramda'); | ||
const StandardError = require('standard-error'); | ||
const RefreshTokenExpired = | ||
new StandardError('The refresh token has expired', {name: 'RefreshTokenExpiredError'}); | ||
const InvalidAccessToken = | ||
new StandardError('The access token provided is invalid', {name: 'InvalidAccessToken'}); | ||
|
||
const Store = t.interface({ | ||
// Signature: (userId, refreshToken) | ||
remove: t.Function, | ||
// Signature: (userId) | ||
removeAll: t.Function, | ||
// Signature: (userId, refreshToken) | ||
getAccessToken: t.Function, | ||
// Signature: (userId, refreshToken, accessToken, ttl) | ||
registerTokens: t.Function | ||
}, 'Stores'); | ||
|
||
const JWT = t.interface({ | ||
// Signature: (payload, secret, {algorithm: String}) | ||
sign: t.Function, | ||
// Signature: (payload, secret, {algorithm: String, otherVerifyOptions}) | ||
verify: t.Function, | ||
// Signature: (payload) | ||
decode: t.Function, | ||
}, 'JWT'); | ||
|
||
// 30 minutes | ||
const regularTokenLifeInSeconds = 60 * 30; | ||
// 1 hour | ||
const tokenLifeUpperLimitInSeconds = 60 * 60; | ||
// 1 day | ||
const regularRefreshTokenLifeInMS = 1000 * 60 * 60 * 24; | ||
// 7 days | ||
const prolongedRefreshTokenLifeInMS = 1000 * 60 * 60 * 24 * 7; | ||
|
||
const Secret = t.refinement(t.String, s => s.length >= 20, 'Secret'); | ||
const ExpiresIn = t.refinement(t.Number, e => e <= tokenLifeUpperLimitInSeconds, 'ExpiresIn'); | ||
const Algorithm = t.enums.of(['HS256', 'HS384', 'HS512', 'RS256'], 'Algorithm'); | ||
|
||
const pld = t.refinement(t.Object, o => typeof o.userId !== 'undefined', 'pld'); | ||
|
||
const VerifyOptions = t.interface({ | ||
audience: t.maybe(t.union([t.String, t.Array, t.Object])), | ||
issuer: t.maybe(t.union([t.String, t.Array])), | ||
ignoreExpiration: t.maybe(t.Boolean), | ||
ignoreNotBefore: t.maybe(t.Boolean), | ||
subject: t.maybe(t.String), | ||
clockTolerance: t.maybe(t.union([t.Number, t.String])), | ||
maxAge: t.maybe(t.union([t.String, t.Number])), | ||
clockTimestamp: t.maybe(t.Number) | ||
}, {name: 'VerifyOptions', strict: true}); | ||
|
||
const UserSignOptions = t.interface({ | ||
nbf: t.maybe(t.Number), | ||
aud: t.maybe(t.String), | ||
iss: t.maybe(t.String), | ||
jti: t.maybe(t.String), | ||
sub: t.maybe(t.String), | ||
}, {name: 'UserSignOption', strict: true}); | ||
|
||
const Payload = UserSignOptions.extend(t.interface({ | ||
pld: pld, | ||
exp: ExpiresIn, | ||
rme: t.Boolean | ||
}, {name: 'Payload', strict: true})); | ||
|
||
const getTTL = rememberMe => | ||
rememberMe ? prolongedRefreshTokenLifeInMS : regularRefreshTokenLifeInMS; | ||
|
||
const getTokensObj = (token, tokenTTL, refreshToken, refreshTokenTTL) => ({ | ||
token, | ||
tokenTTL, | ||
refreshToken, | ||
refreshTokenTTL | ||
}); | ||
|
||
module.exports = class JWTPlus { | ||
/** | ||
* Constructor | ||
* @param {Object} store | ||
* @param {string} [algorithm='HS256] algorithm cannot be 'none' | ||
* @param {Number} [expiresIn=60 * 30] expiration time in seconds. | ||
* @param {Object} [jwt] jsonwebtoken instance, by default it uses require('jsonwebtoken') | ||
* @param {Object} [defaultSignInOptions] | ||
* @param {Object} [defaultVerifyOptions] | ||
*/ | ||
constructor({ | ||
store, algorithm = 'HS256', expiresIn = regularTokenLifeInSeconds, jwt = jsonwebtoken, | ||
defaultSignInOptions = {}, defaultVerifyOptions = {} | ||
}) { | ||
this._store = Store(store); | ||
this._defaultSignInOptions = UserSignOptions(defaultSignInOptions); | ||
this._defaultVerifyOptions = VerifyOptions(defaultVerifyOptions); | ||
this._algorithm = Algorithm(algorithm); | ||
this._expiresIn = ExpiresIn(expiresIn); | ||
this._jwt = JWT(jwt); | ||
} | ||
|
||
/** | ||
* @private | ||
* A private function that creates a refresh token | ||
* @param {String|Number} userId | ||
* @param {String} accessToken | ||
* @param {Number} ttl time to live in milliseconds | ||
* @returns {Promise} | ||
*/ | ||
async _createRefreshToken(userId, accessToken, ttl) { | ||
const refreshToken = generator.generate(256); | ||
await this._store.registerTokens(userId, refreshToken, accessToken, ttl); | ||
return refreshToken; | ||
} | ||
|
||
/** | ||
* Returns access and refresh tokens | ||
* @param {Object} content token's payload | ||
* @param secret | ||
* @param {Boolean} rememberMe if true, the token will last 7 days instead of 1. | ||
* @param {Object} [signOptions] Options to be passed to jwt.sign | ||
* @returns {Promise<{ | ||
* token: *, tokenTTL: Number, refreshToken: *, refreshTokenTTL: Number | ||
* }>} | ||
*/ | ||
async sign(content, secret, rememberMe = false, signOptions = {}) { | ||
const token = this._jwt.sign( | ||
// Payload | ||
Payload({pld: content, | ||
...mergeAll([ | ||
this._defaultSignInOptions, UserSignOptions(signOptions), | ||
{exp: this._expiresIn, rme: rememberMe} | ||
])}), | ||
// Secret | ||
Secret(secret), | ||
// Options | ||
{algorithm: this._algorithm}); | ||
const ttl = getTTL(rememberMe); | ||
return getTokensObj(token, | ||
this._expiresIn, | ||
await this._createRefreshToken(content.userId, token, ttl), | ||
ttl); | ||
} | ||
|
||
/** | ||
* Verifies token, might throw jwt.verify errors | ||
* @param {String} token | ||
* @param secret | ||
* @param {Object} [verifyOptions] Options to pass to jwt.verify. | ||
* @returns {Promise<*>} | ||
*/ | ||
verify(token, secret, verifyOptions = {}) { | ||
return this._jwt.verify(token, Secret(secret), | ||
mergeAll([this._defaultVerifyOptions, VerifyOptions(verifyOptions), | ||
{algorithm: this._algorithm}])); | ||
} | ||
|
||
/** | ||
* Issues a new access token using a refresh token and an old token. | ||
* There is no need to verify the old token provided because this method uses the stored one. | ||
* @param {String} refreshToken | ||
* @param {String} oldToken | ||
* @param secret | ||
* @param {Object} [signOptions] Options passed to jwt.sign | ||
* @returns {Promise<*>} | ||
*/ | ||
async refresh(refreshToken, oldToken, secret, signOptions) { | ||
t.String(refreshToken); | ||
t.String(oldToken); | ||
const untrustedPayload = Payload(this._jwt.decode(oldToken).payload); | ||
const trustedToken = await this._store.getAccessToken(untrustedPayload.userId, refreshToken); | ||
// Remove the refresh token even if the following operations were not successful. | ||
// RefreshTokens are one time use only | ||
if(!await this._store.remove(untrustedPayload.userId, refreshToken)) { | ||
throw RefreshTokenExpired; | ||
} | ||
// RefreshTokens works with only one AccessToken | ||
if (trustedToken !== oldToken) {throw InvalidAccessToken;} | ||
|
||
// Token is safe since it is stored by us | ||
const {payload: {pld: payload, rme: rememberMe, ...jwtOptions}} = | ||
this._jwt.decode(trustedToken); | ||
|
||
// Finally, sign new tokens for the user | ||
return this.sign( | ||
payload, | ||
Secret(secret), | ||
rememberMe, | ||
// Ignoring exp | ||
UserSignOptions({...dissoc('exp', jwtOptions), ...signOptions}) | ||
); | ||
} | ||
|
||
/** | ||
* Invalidates refresh token | ||
* @param {String|Number} userId | ||
* @param {String} refreshToken | ||
* @returns {Promise} | ||
*/ | ||
invalidateRefreshToken(userId, refreshToken) { | ||
return this._store.remove(userId, refreshToken); | ||
} | ||
|
||
/** | ||
* Invalidates all refresh tokens | ||
* @param {String|Number} userId | ||
* @returns {Promise} | ||
*/ | ||
invalidateAllRefreshTokens(userId) {return this._store.removeAll(userId);} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "jwt-plus", | ||
"version": "0.0.0", | ||
"description": "An opinionated JWT library with sensible defaults that implements the complete token flow.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "npm run test:lint && npm run test:coverage", | ||
"test:coverage": "jest tests --coverage", | ||
"test:lint": "eslint tests index.js" | ||
}, | ||
"repository": "https://github.com/wearereasonablepeople/jwt-plus", | ||
"author": "Abdulrahman Amri", | ||
"license": "MIT", | ||
"dependencies": { | ||
"jsonwebtoken": "latest", | ||
"ramda": "^0.25.0", | ||
"rand-token": "^0.4.0", | ||
"standard-error": "^1.1.0", | ||
"tcomb": "^3.2.24" | ||
}, | ||
"devDependencies": { | ||
"codecov": "^3.0.0", | ||
"eslint": "^4.15.0", | ||
"eslint-config-warp": "^2.1.0", | ||
"jest": "^21.0.0", | ||
"ms": "latest" | ||
} | ||
} |
Oops, something went wrong.