-
Notifications
You must be signed in to change notification settings - Fork 0
init #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
init #2
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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. | ||
[](https://travis-ci.com/wearereasonablepeople/jwt-plus) | ||
[](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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add
"root": true
to prevent extension of unrelated configs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍