├── .gitignore ├── .npmrc ├── .eslintrc.json ├── tests ├── testUtilities.js ├── index.api.test.js ├── index.integration.test.js └── index.test.js ├── jest.config.js ├── .github └── workflows │ └── node.js.yml ├── package.json ├── examples └── koa.js ├── jsdoc2md └── readme.hbs ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["warp/node", "warp/es6"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018 6 | }, 7 | "overrides": [ 8 | { 9 | "files": [ "tests/*.js" ], 10 | "env": { 11 | "jest": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/testUtilities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.fakeStore = { 4 | // Signature: (userId, refreshTokenJTI) 5 | remove: jest.fn(() => true), 6 | // Signature: (userId) 7 | removeAll: jest.fn(() => true), 8 | // Signature: (userId, refreshTokenJTI, accessTokenJTI, ttl) 9 | add: jest.fn(() => true), 10 | }; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | collectCoverageFrom: [ 5 | 'index.js', 6 | '!**/node_modules/**', 7 | '!**/vendor/**', 8 | '!examples/**' 9 | ], 10 | coverageThreshold: { 11 | global: { 12 | branches: 100, 13 | functions: 100, 14 | lines: 100, 15 | statements: 100 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | services: 23 | redis: 24 | image: redis 25 | ports: 26 | - 6379:6379 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - run: npm i 35 | - run: npm test 36 | - name: Coveralls 37 | uses: coverallsapp/github-action@master 38 | with: 39 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authomatic", 3 | "version": "1.0.2", 4 | "description": "An authentication library that uses JWT for access and refresh tokens with sensible defaults.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run test:lint && npm run test:coverage", 8 | "test:coverage": "jest tests -i --coverage", 9 | "test:lint": "eslint tests index.js", 10 | "docs": "jsdoc2md -t jsdoc2md/readme.hbs index.js > README.md", 11 | "redis": "npm run redis:remove && npm run redis:start", 12 | "redis:start": "docker run --name redis-test -p 6379:6379 -d redis", 13 | "redis:remove": "docker rm -f redis-test &> /dev/null || true" 14 | }, 15 | "repository": "https://github.com/amri91/authomatic", 16 | "author": "Abdulrahman Amri", 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=10.x" 20 | }, 21 | "dependencies": { 22 | "jsonwebtoken": "^8.5.1", 23 | "standard-error": "^1.1.0", 24 | "tcomb": "^3.2.29" 25 | }, 26 | "devDependencies": { 27 | "authomatic-redis": "^1.0.1", 28 | "coveralls": "^3.1.1", 29 | "eslint-config-warp": "^6.1.0", 30 | "eslint-plugin-node": "^11.1.0", 31 | "eslint": "^7.12.1", 32 | "jest": "^27.5.0", 33 | "jsdoc-to-markdown": "^7.1.1", 34 | "koa": "^2.13.4", 35 | "koa-bodyparser": "^4.3.0", 36 | "koa-router": "^10.1.1", 37 | "ramda": "^0.28.0", 38 | "redis": "^3.1.1", 39 | "redisscan": "^2.0.0", 40 | "supertest": "^6.2.2" 41 | }, 42 | "keywords": [ 43 | "authentication", 44 | "jwt", 45 | "refresh-token", 46 | "security" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /examples/koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const secret = 'thisIsAVeryBadSecret'; 4 | const scope = 'Admin'; 5 | 6 | const Koa = require('koa'); 7 | const bodyParser = require('koa-bodyparser'); 8 | const Router = require('koa-router'); 9 | 10 | const Store = require('authomatic-redis'); 11 | const Authomatic = require('../index'); 12 | 13 | const store = Store(); 14 | const authomatic = new Authomatic({ 15 | store, 16 | defaultSignOptions: { 17 | // Set iss property to A for all signed tokens 18 | iss: 'A' 19 | }, 20 | defaultVerifyOptions: { 21 | // Accept tokens issued by A and B 22 | issuer: ['A', 'B'] 23 | } 24 | }); 25 | 26 | const getBearer = ({request: {headers}}) => { 27 | if(headers.authorization) { 28 | return headers.authorization.replace('Bearer ', ''); 29 | } 30 | return ''; 31 | }; 32 | 33 | const verify = (ctx, next) => { 34 | const accessToken = getBearer(ctx); 35 | if(!accessToken) { 36 | return ctx.throw(400, 'missing authorization header'); 37 | } 38 | try { 39 | // Returns decoded token 40 | authomatic.verify(accessToken, secret); 41 | return next(); 42 | } catch (e) { 43 | return ctx.throw( 44 | ['JsonWebTokenError', 'InvalidToken', 'TokenExpiredError'].includes(e.name) ? 401 : 500, 45 | e.message 46 | ); 47 | } 48 | }; 49 | 50 | const router = new Router({ 51 | prefix: `/tokens` 52 | }); 53 | 54 | router 55 | .post('/login', async ctx => { 56 | const {rememberMe} = ctx.request.body; 57 | // Add verify credentials logic 58 | ctx.body = await authomatic.sign( 59 | '123', 60 | secret, 61 | {/*Put any extra static content*/scopes: [scope]}, 62 | rememberMe 63 | ); 64 | ctx.status = 201; 65 | }) 66 | .post('/refresh', async ctx => { 67 | const {accessToken, refreshToken} = ctx.request.body; 68 | 69 | try { 70 | ctx.body = await authomatic.refresh(refreshToken, accessToken, secret, {}); 71 | } catch (e) { 72 | ctx.throw( 73 | [ 74 | 'RefreshTokenNotFound', 'TokensMismatch', 'TokenExpiredError', 75 | 'JsonWebTokenError', 'InvalidToken' 76 | ].includes(e.name) ? 400 : 500, 77 | e.message 78 | ); 79 | } 80 | }) 81 | .delete('/refreshTokens/:refreshToken', async ctx => { 82 | const {refreshToken} = ctx.params; 83 | 84 | if(await authomatic.invalidateRefreshToken(decodeURIComponent(refreshToken), secret)) { 85 | ctx.status = 204; 86 | } else { 87 | ctx.status = 404; 88 | } 89 | }) 90 | .use(verify) 91 | // All private routes under this point 92 | .delete('/refreshTokens', async ctx => { 93 | const {userId} = ctx.request.body; 94 | // You need to check if the user is authorized to 95 | // delete all refresh tokens for the provided user id 96 | if(await authomatic.invalidateAllRefreshTokens(userId)) { 97 | ctx.status = 204; 98 | } else { 99 | ctx.status = 404; 100 | } 101 | }); 102 | 103 | exports.app = new Koa(); 104 | exports.app 105 | .use(bodyParser()) 106 | .use(router.allowedMethods({throw: true})) 107 | .use(router.routes()); 108 | 109 | exports.authomatic = authomatic; 110 | exports.store = store; 111 | -------------------------------------------------------------------------------- /jsdoc2md/readme.hbs: -------------------------------------------------------------------------------- 1 | # authomatic 2 | [![Build Status](https://travis-ci.org/wearereasonablepeople/authomatic.svg?branch=master)](https://travis-ci.org/wearereasonablepeople/authomatic) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/314b595549aca68c5c6c/maintainability)](https://codeclimate.com/github/wearereasonablepeople/authomatic/maintainability) 4 | [![Coverage Status](https://coveralls.io/repos/github/wearereasonablepeople/authomatic/badge.svg?branch=master)](https://coveralls.io/github/wearereasonablepeople/authomatic?branch=master) 5 | [![dependencies Status](https://david-dm.org/wearereasonablepeople/authomatic/status.svg)](https://david-dm.org/wearereasonablepeople/authomatic) 6 | [![devDependencies Status](https://david-dm.org/awearereasonablepeople/authomatic/dev-status.svg)](https://david-dm.org/wearereasonablepeople/authomatic?type=dev) 7 | [![Greenkeeper badge](https://badges.greenkeeper.io/wearereasonablepeople/authomatic.svg)](https://greenkeeper.io/) 8 | 9 | ## Description 10 | An authentication library that uses JWT for access and refresh tokens with sensible defaults. 11 | 12 | ## Install 13 | ``` 14 | npm install authomatic 15 | ``` 16 | 17 | ## Available stores 18 | [Redis](https://github.com/wearereasonablepeople/authomatic-redis) 19 | 20 | Please create an issue if you need another store. 21 | 22 | ## Examples 23 | [Koa Example](/examples/koa.js) 24 | 25 | ## Quickstart 26 | ```javascript 27 | const Store = require('authomatic-redis'); 28 | const Authomatic = require('authomatic'); 29 | const store = Store(); 30 | const authomatic = new Authomatic({store}); 31 | 32 | // Use authomatic functions 33 | ``` 34 | 35 | ## Test 36 | ``` 37 | npm test 38 | ``` 39 | 40 | ## Notes about migrating from version 0.0.1 to 1 41 | 1. Access and refresh tokens from those old versions will not work with the new ones. If you just upgraded, users will be required to relog. 42 | If that is undesirable, and you want a seamless transition use two instances of Authomatic, but do not sign new tokens (or refresh) with the old instance. 43 | 1. The refresh method now accepts a 4th argument, verify options. 44 | 1. The invalidate refresh token method now requires a secret. 45 | 1. aud in sign options and audience in verify options are now strictly an array. 46 | 1. RefreshTokenExpiredOrNotFound became RefreshTokenNotFound, the expiration error is throw by the 'jsonwebtoken' library. 47 | 1. InvalidAccessToken became InvalidToken, it is for both refresh and access tokens. 48 | 1. TokensMismatch error is thrown if refresh and access token do not match. 49 | 50 | The example has been updated to reflect all the new changes. 51 | 52 | # Documentation 53 | 54 | {{>main}} 55 | 56 | # Creating a store 57 | If you want to create a new store you need to expose the following functions: 58 | 59 | 1. add 60 | 61 | ```js 62 | /** 63 | * Register token and refresh token to the user 64 | * @param {String} userId 65 | * @param {String} refreshTokenJTI 66 | * @param {String} accessTokenJTI 67 | * @param {Number} ttl time to live in ms 68 | * @returns {Promise} returns true when created. 69 | */ 70 | function add(userId, refreshTokenJTI, accessTokenJTI, ttl){...} 71 | ``` 72 | 73 | 2. remove 74 | ```js 75 | /** 76 | * Remove a single refresh token from the user 77 | * @param userId 78 | * @param refreshTokenJTI 79 | * @returns {Promise} true if found and deleted, otherwise false. 80 | */ 81 | function remove(userId, refreshTokenJTI) {...} 82 | ``` 83 | 84 | 3. removeAll 85 | ```js 86 | /** 87 | * Removes all tokens for a particular user 88 | * @param userId 89 | * @returns {Promise} true if any were found and delete, false otherwise 90 | */ 91 | function removeAll(userId) {...} 92 | ``` 93 | You may need to expose a reference to the store if the user may need to handle connections during testing for example. 94 | -------------------------------------------------------------------------------- /tests/index.api.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('supertest'); 4 | const secret = 'thisIsAVeryBadSecret'; 5 | 6 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 7 | 8 | const {app, authomatic, store} = require('../examples/koa'); 9 | 10 | const getTokens = () => authomatic.sign('111', secret); 11 | 12 | describe('Authomatic', () => { 13 | 14 | beforeEach(done => { 15 | store.client.flushall(err => { 16 | done(err); 17 | }); 18 | }); 19 | 20 | afterAll(done => { 21 | store.client.quit(err => { 22 | done(err); 23 | }); 24 | }); 25 | 26 | describe('#revokeRefreshToken', () => { 27 | it('should revoke the refreshToken if found and return 404 afterwards', async () => { 28 | const {refreshToken} = await getTokens(); 29 | await request(app.callback()) 30 | .delete(`/tokens/refreshTokens/${encodeURIComponent(refreshToken)}`) 31 | .expect(204); 32 | await request(app.callback()) 33 | .delete(`/tokens/refreshTokens/${encodeURIComponent(refreshToken)}`) 34 | .expect(404); 35 | }); 36 | }); 37 | 38 | describe('#login', () => { 39 | it('should return token pairs when logging in', async () => { 40 | const {body} = await request(app.callback()) 41 | .post(`/tokens/login`) 42 | .send({rememberMe: false}) 43 | .expect(201); 44 | expect(body.accessToken && body.refreshToken).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe('#verify', () => { 49 | it('should not allow unauthenticated users to access private routes', async () => { 50 | await request(app.callback()) 51 | .delete(`/tokens/refreshTokens`) 52 | .set('Authorization', 'Bearer 123') 53 | .send({userId: '123'}) 54 | .expect(401); 55 | }); 56 | it('should return 400 if the authorization header was not set', async () => { 57 | await request(app.callback()) 58 | .delete(`/tokens/refreshTokens`) 59 | .send({userId: '123'}) 60 | .expect(400); 61 | }); 62 | }); 63 | 64 | describe('#revokeAllTokens', () => { 65 | it('should revokeAllTokens for the provided userId', async () => { 66 | const {accessToken} = await getTokens(); 67 | await request(app.callback()) 68 | .delete(`/tokens/refreshTokens`) 69 | .set('Authorization', `Bearer ${accessToken}`) 70 | .send({userId: '111'}) 71 | .expect(204); 72 | 73 | await request(app.callback()) 74 | .delete(`/tokens/refreshTokens`) 75 | .set('Authorization', `Bearer ${accessToken}`) 76 | .send({userId: '111'}) 77 | .expect(404); 78 | }); 79 | }); 80 | 81 | describe('#refresh', () => { 82 | it('should generate a new pair of tokens and remove the old refresh token', async () => { 83 | const {accessToken, refreshToken} = await getTokens(); 84 | await request(app.callback()) 85 | .post(`/tokens/refresh`) 86 | .send({accessToken, refreshToken}) 87 | .expect(200); 88 | 89 | await request(app.callback()) 90 | .post(`/tokens/refresh`) 91 | .send({accessToken, refreshToken}) 92 | .expect(400); 93 | }); 94 | 95 | it('should generate handle bad refresh tokens', async () => { 96 | const {accessToken} = await getTokens(); 97 | await request(app.callback()) 98 | .post(`/tokens/refresh`) 99 | .send({accessToken, refreshToken: ''}) 100 | .expect(400); 101 | }); 102 | 103 | it('should not allow mismatching token pairs', async () => { 104 | const {accessToken: oldToken, refreshToken} = await getTokens(); 105 | await sleep(2000); 106 | const {accessToken} = await getTokens(); 107 | expect(oldToken !== accessToken).toBeTruthy(); 108 | await request(app.callback()) 109 | .post(`/tokens/refresh`) 110 | .send({accessToken, refreshToken}) 111 | .expect(400); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/index.integration.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const Authomatic = require('../index'); 6 | const testUtilities = require('./testUtilities'); 7 | 8 | const algorithm = 'HS256'; 9 | 10 | const userId = '123'; 11 | const secret = 'asdfasdfasdfasdf1234'; 12 | const computeExpiryDate = seconds => Math.floor(Date.now() / 1000) + seconds; 13 | 14 | const createFakeAccessToken = (jti, exp, alg) => 15 | jwt.sign( 16 | { 17 | pld: {}, uid: userId, jti: jti || 'atJTI', 18 | exp: exp || computeExpiryDate(10), t: 'Authomatic-AT' 19 | }, 20 | secret, {algorithm: alg || algorithm} 21 | ); 22 | 23 | const createFakeRefreshToken = (jti, atJTI, exp) => 24 | jwt.sign( 25 | { 26 | aud: ['Authomatic'], iss: 'Authomatic', 27 | uid: userId, jti: jti || 'rtJTI', accessTokenJTI: atJTI || 'atJTI', 28 | exp: exp || computeExpiryDate(100), t: 'Authomatic-RT' 29 | }, 30 | secret, {algorithm} 31 | ); 32 | 33 | const accessToken = createFakeAccessToken(); 34 | const refreshToken = createFakeRefreshToken(); 35 | 36 | describe('authomatic', () => { 37 | let authomatic, fakeStore; 38 | 39 | beforeEach(() => { 40 | fakeStore = testUtilities.fakeStore; 41 | authomatic = Authomatic({store: fakeStore, algorithm, jwt}); 42 | }); 43 | 44 | describe('#verify', () => { 45 | it('Should verify and decode tokens', async () => { 46 | const exp = computeExpiryDate(10); 47 | expect(await authomatic.verify(createFakeAccessToken('jti', exp), secret)) 48 | .toEqual({ 49 | pld: {}, uid: userId, jti: 'jti', 50 | exp, iat: exp - 10, t: 'Authomatic-AT' 51 | }); 52 | }); 53 | it('Should fail if the algorithm mismatch', async () => { 54 | expect.assertions(1); 55 | try { 56 | await authomatic.verify(createFakeAccessToken(null, null, 'RS256'), secret); 57 | } catch (e) { 58 | expect(e).toBeTruthy(); 59 | } 60 | }); 61 | it('Should fail when trying to verify refresh tokens instead of access tokens', async () => { 62 | expect.assertions(1); 63 | try { 64 | await authomatic.verify(createFakeRefreshToken(), secret); 65 | } catch (e) { 66 | expect(e.name).toBe('InvalidToken'); 67 | } 68 | }); 69 | }); 70 | describe('#refresh', () => { 71 | it('Should return a new pair of valid tokens', async () => { 72 | const results = 73 | await authomatic.refresh(refreshToken, accessToken, secret, {}); 74 | expect(authomatic.verify(results.accessToken, secret)).toBeTruthy(); 75 | expect( 76 | authomatic.refresh(results.refreshToken, results.accessToken, secret, {}) 77 | ).toBeTruthy(); 78 | }); 79 | it('Should not refresh mismatching tokens', async () => { 80 | expect.assertions(1); 81 | const mismatchingAccessToken = 82 | jwt.sign( 83 | {pld: {}, uid: userId, jti: 'mismatchingJTI', t: 'Authomatic-AT'}, secret, {algorithm} 84 | ); 85 | try { 86 | await authomatic.refresh(refreshToken, mismatchingAccessToken, secret, {}); 87 | } catch(e) { 88 | expect(e.name).toBe('TokensMismatch'); 89 | } 90 | }); 91 | it('Should throw an error if the refresh token expired', async () => { 92 | expect.assertions(1); 93 | try { 94 | await authomatic.refresh( 95 | createFakeRefreshToken(0, 0, computeExpiryDate(-10)), null, secret, {} 96 | ); 97 | } catch(e) { 98 | expect(e.name).toBe('TokenExpiredError'); 99 | } 100 | }); 101 | it('Should throw an error if refresh token and access tokens were swaped', async () => { 102 | expect.assertions(1); 103 | try { 104 | await authomatic.refresh(createFakeAccessToken(), createFakeRefreshToken(), secret, {} 105 | ); 106 | } catch(e) { 107 | expect(e.name).toBe('InvalidToken'); 108 | } 109 | }); 110 | it('Should accept expired access tokens', async () => { 111 | expect(await authomatic.refresh( 112 | createFakeRefreshToken(0, 0, computeExpiryDate()), 113 | createFakeAccessToken(0, computeExpiryDate(-10)), secret, {} 114 | )).toBeTruthy(); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {omit, mergeRight} = require('ramda'); 4 | 5 | const testUtilities = require('./testUtilities'); 6 | 7 | const Authomatic = require('../index'); 8 | 9 | const customAlgorithm = 'HS256'; 10 | 11 | const acceptableSignOptions = { 12 | algorithm: customAlgorithm 13 | }; 14 | 15 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 16 | 17 | describe('authomatic', () => { 18 | let authomatic, fakeStore, fakeJWT; 19 | const userId = '123', secret = 'asdfasdfasdfasdf1234', refreshToken = { 20 | aud: ['Authomatic'], iss: 'Authomatic', 21 | uid: userId, jti: 'rtJTI', accessTokenJTI: 'atJTI', 22 | exp: 123, t: 'Authomatic-RT' 23 | }; 24 | 25 | beforeEach(() => { 26 | fakeStore = testUtilities.fakeStore; 27 | fakeJWT = { 28 | verify: jest.fn(t => t), 29 | sign: jest.fn(t => t), 30 | decode: jest.fn(t => t) 31 | }; 32 | authomatic = Authomatic({store: fakeStore, algorithm: customAlgorithm, jwt: fakeJWT}); 33 | }); 34 | 35 | describe('#constructor', () => { 36 | it('should not allow incomplete store object', () => { 37 | // Store missing the remove function 38 | expect(() => Authomatic({store: omit(['remove'], fakeStore)})).toThrow(); 39 | }); 40 | it('should not allow the none algorithm', () => { 41 | expect(() => Authomatic({algorithm: 'none'})).toThrow(); 42 | }); 43 | it('should not accept values greater than an hour', () => { 44 | expect(() => Authomatic({expiresIn: 60 * 60 * 12})).toThrow(); 45 | }); 46 | }); 47 | describe('#invalidateRefreshToken', () => { 48 | it('Should be true on success', () => { 49 | expect(authomatic.invalidateRefreshToken(refreshToken, secret)).toBe(true); 50 | }); 51 | it('Should be false if token was not found', () => { 52 | // Make remove unsuccessful 53 | authomatic = Authomatic( 54 | {store: mergeRight(fakeStore, {remove: jest.fn(() => false)}), jwt: fakeJWT} 55 | ); 56 | expect(authomatic.invalidateRefreshToken(refreshToken, secret)).toBe(false); 57 | }); 58 | }); 59 | describe('#invalidateAllRefreshTokens', () => { 60 | it('should instruct the store to remove all refresh tokens', () => { 61 | authomatic.invalidateAllRefreshTokens(userId); 62 | expect(fakeStore.removeAll.mock.calls[0][0]).toBe(userId); 63 | }); 64 | it('Should be truthy on success', () => { 65 | expect(authomatic.invalidateAllRefreshTokens(userId)).toBe(true); 66 | }); 67 | it('Should be falsey if no tokens were found', () => { 68 | // make removeAll unsuccessful 69 | authomatic = Authomatic({store: mergeRight(fakeStore, {removeAll: jest.fn(() => false)})}); 70 | expect(authomatic.invalidateAllRefreshTokens(userId)).toBe(false); 71 | }); 72 | }); 73 | describe('#sign', () => { 74 | it('should instruct jwt.sign to sign a token with correct arguments', async () => { 75 | await authomatic.sign('123', secret); 76 | const {exp, jti, ...payload} = fakeJWT.sign.mock.calls[0][0]; 77 | expect(exp).toBeTruthy(); 78 | expect(jti).toBeTruthy(); 79 | expect(payload).toEqual({pld: {}, rme: false, uid: '123', t: 'Authomatic-AT'}); 80 | expect(fakeJWT.sign.mock.calls[0][1]).toBe(secret); 81 | expect(fakeJWT.sign.mock.calls[0][2]).toEqual(acceptableSignOptions); 82 | }); 83 | it('should allow payload to contain unstrictly defined properties', async () => { 84 | const content = {someProp: '123'}; 85 | await authomatic.sign('123', secret, content); 86 | const {exp, jti, ...payload} = fakeJWT.sign.mock.calls[0][0]; 87 | expect(exp).toBeTruthy(); 88 | expect(jti).toBeTruthy(); 89 | expect(payload.pld).toEqual(content); 90 | expect(fakeJWT.sign.mock.calls[0][1]).toBe(secret); 91 | expect(fakeJWT.sign.mock.calls[0][2]).toEqual(acceptableSignOptions); 92 | }); 93 | it('should return correct object', async () => { 94 | const object = await authomatic.sign('123', secret); 95 | expect(object).toEqual(expect.objectContaining({ 96 | accessToken: expect.anything(), 97 | accessTokenExpiresAt: expect.any(Number), 98 | refreshToken: expect.anything(), 99 | refreshTokenExpiresAt: expect.any(Number) 100 | })); 101 | }); 102 | it('should prolong the refreshToken ttl when rememberMe is true', async () => { 103 | const {refreshTokenExpiresAt: longTTL} = await authomatic.sign('123', secret, {}, true); 104 | const {refreshTokenExpiresAt: shortTTL} = await authomatic.sign('123', secret); 105 | expect(longTTL > shortTTL).toBeTruthy(); 106 | }); 107 | it('should recalculate current time every time a new pair of tokens are created. Issue #21', 108 | async () => { 109 | const {accessTokenExpiresAt: firstAT, refreshTokenExpiresAt: firstRT} = 110 | await authomatic.sign('123', secret); 111 | await sleep(2000); 112 | const {accessTokenExpiresAt: secondAT, refreshTokenExpiresAt: secondRT} = 113 | await authomatic.sign('123', secret); 114 | expect(firstAT < secondAT).toBeTruthy(); 115 | expect(firstRT < secondRT).toBeTruthy(); 116 | } 117 | ); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # authomatic 2 | [![Build Status](https://travis-ci.org/Amri91/authomatic.svg?branch=master)](https://travis-ci.org/Amri91/authomatic) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/d990daf2cfee6fd1cb3e/maintainability)](https://codeclimate.com/github/Amri91/authomatic/maintainability) 4 | 5 | ## Description 6 | An authentication library that uses JWT for access and refresh tokens with sensible defaults. 7 | 8 | ## Install 9 | ``` 10 | npm install authomatic 11 | ``` 12 | 13 | ## Available stores 14 | [Redis](https://github.com/amri91/authomatic-redis) 15 | 16 | Please create an issue if you need another store. 17 | 18 | ## Examples 19 | [Koa Example](/examples/koa.js) 20 | 21 | ## Quickstart 22 | ```javascript 23 | const Store = require('authomatic-redis'); 24 | const Authomatic = require('authomatic'); 25 | const store = Store(); 26 | const authomatic = new Authomatic({store}); 27 | 28 | // Use authomatic functions 29 | ``` 30 | 31 | ## Test 32 | ``` 33 | npm test 34 | ``` 35 | 36 | ## Notes about migrating from version 0.0.1 to 1 37 | 1. Access and refresh tokens from those old versions will not work with the new ones. If you just upgraded, users will be required to relog. 38 | If that is undesirable, and you want a seamless transition use two instances of Authomatic, but do not sign new tokens (or refresh) with the old instance. 39 | 1. The refresh method now accepts a 4th argument, verify options. 40 | 1. The invalidate refresh token method now requires a secret. 41 | 1. aud in sign options and audience in verify options are now strictly an array. 42 | 1. RefreshTokenExpiredOrNotFound became RefreshTokenNotFound, the expiration error is throw by the 'jsonwebtoken' library. 43 | 1. InvalidAccessToken became InvalidToken, it is for both refresh and access tokens. 44 | 1. TokensMismatch error is thrown if refresh and access token do not match. 45 | 46 | The example has been updated to reflect all the new changes. 47 | 48 | # Documentation 49 | 50 | ## Members 51 | 52 |
53 |
signPromise.<Tokens>
54 |

Returns access and refresh tokens

55 |
56 |
verifyString
57 |

Verifies token, might throw jwt.verify errors

58 |
59 |
refreshPromise.<Tokens>
60 |

Issues a new access token using a refresh token and an old token (can be expired).

61 |
62 |
invalidateRefreshTokenPromise.<Boolean>
63 |

Invalidates refresh token

64 |
65 |
invalidateAllRefreshTokensPromise.<Boolean>
66 |

Invalidates all refresh tokens

67 |
68 |
69 | 70 | ## Typedefs 71 | 72 |
73 |
Secret : String
74 |

a string greater than 20 characters

75 |
76 |
AccessToken : String
77 |

Regular JWT token. 78 | Its payload looks like this:

79 |
{
 80 |   "t": "Authomatic-AT",
 81 |   "uid": "userId",
 82 |   "exp": "someNumber",
 83 |   "jti": "randomBytes",
 84 |   ...otherClaims,
 85 |   "pld": {
 86 |     ...otherUserContent
 87 |   }
 88 | }
 89 | 
90 |
91 |
RefreshToken : String
92 |

regular JWT token. 93 | Its payload looks like this:

94 |
 {
 95 |    "t": "Authomatic-RT",
 96 |    "iss": "Authomatic",
 97 |    "aud": ["Authomatic"]
 98 |    "uid": "userId",
 99 |    "exp": "someNumber",
100 |    "jti": "randomBytes",
101 |    "accessTokenJTI": "randomBytes"
102 |  }
103 | 
104 |
105 |
Tokens : Object
106 |

Token pairs

107 |
108 |
VerifyOptions : Object
109 |

Verify options to be used when verifying tokens

110 |
111 |
SignOptions : Object
112 |

The allowed user options to for signing tokens

113 |
114 |
RefreshTokenNotFound : StandardError
115 |

The refresh token was not found.

116 |
117 |
TokensMismatch : StandardError
118 |

The tokens provided do not match

119 |
120 |
InvalidToken : StandardError
121 |

The provided input is not a valid token.

122 |
123 |
124 | 125 | 126 | 127 | ## sign ⇒ [Promise.<Tokens>](#Tokens) 128 | Returns access and refresh tokens 129 | 130 | **Kind**: global variable 131 | **Throws**: 132 | 133 | - TypeError typeError if any param was not sent exactly as specified 134 | 135 | 136 | | Param | Type | Description | 137 | | --- | --- | --- | 138 | | userId | String | | 139 | | secret | [Secret](#Secret) | | 140 | | [content] | Object | user defined properties | 141 | | [prolong] | Boolean | if true, the refreshToken will last 4 days and accessToken 1 hour, otherwise the refresh token will last 25 minutes and the accessToken 15 minutes. | 142 | | [signOptions] | [SignOptions](#SignOptions) | Options to be passed to jwt.sign | 143 | 144 | 145 | 146 | ## verify ⇒ String 147 | Verifies token, might throw jwt.verify errors 148 | 149 | **Kind**: global variable 150 | **Returns**: String - decoded token 151 | **Throws**: 152 | 153 | - [InvalidToken](#InvalidToken) invalidToken 154 | - TypeError typeError if any param was not sent exactly as specified 155 | - JsonWebTokenError 156 | - TokenExpiredError 157 | Error info at [https://www.npmjs.com/package/jsonwebtoken#errors--codes](https://www.npmjs.com/package/jsonwebtoken#errors--codes) 158 | 159 | 160 | | Param | Type | Description | 161 | | --- | --- | --- | 162 | | token | String | | 163 | | secret | [Secret](#Secret) | | 164 | | [verifyOptions] | [VerifyOptions](#VerifyOptions) | Options to pass to jwt.verify. | 165 | 166 | 167 | 168 | ## refresh ⇒ [Promise.<Tokens>](#Tokens) 169 | Issues a new access token using a refresh token and an old token (can be expired). 170 | 171 | **Kind**: global variable 172 | **Throws**: 173 | 174 | - [RefreshTokenNotFound](#RefreshTokenNotFound) refreshTokenNotFound 175 | - [TokensMismatch](#TokensMismatch) tokensMismatch 176 | - TypeError typeError if any param was not sent exactly as specified 177 | - JsonWebTokenError 178 | - TokenExpiredError 179 | Error info at [https://www.npmjs.com/package/jsonwebtoken#errors--codes](https://www.npmjs.com/package/jsonwebtoken#errors--codes) 180 | 181 | 182 | | Param | Type | Description | 183 | | --- | --- | --- | 184 | | refreshToken | String | | 185 | | accessToken | String | | 186 | | secret | [Secret](#Secret) | | 187 | | signOptions | [SignOptions](#SignOptions) | Options passed to jwt.sign, ignoreExpiration will be set to true | 188 | 189 | 190 | 191 | ## invalidateRefreshToken ⇒ Promise.<Boolean> 192 | Invalidates refresh token 193 | 194 | **Kind**: global variable 195 | **Returns**: Promise.<Boolean> - true if successful, false otherwise. 196 | **Throws**: 197 | 198 | - TypeError typeError if any param was not sent exactly as specified 199 | - [InvalidToken](#InvalidToken) invalidToken 200 | - JsonWebTokenError 201 | - TokenExpiredError 202 | Error info at [https://www.npmjs.com/package/jsonwebtoken#errors--codes](https://www.npmjs.com/package/jsonwebtoken#errors--codes) 203 | 204 | 205 | | Param | Type | 206 | | --- | --- | 207 | | refreshToken | String | 208 | 209 | 210 | 211 | ## invalidateAllRefreshTokens ⇒ Promise.<Boolean> 212 | Invalidates all refresh tokens 213 | 214 | **Kind**: global variable 215 | **Returns**: Promise.<Boolean> - true if successful, false otherwise. 216 | **Throws**: 217 | 218 | - TypeError typeError if any param was not sent exactly as specified 219 | 220 | 221 | | Param | Type | 222 | | --- | --- | 223 | | userId | String | 224 | 225 | 226 | 227 | ## Secret : String 228 | a string greater than 20 characters 229 | 230 | **Kind**: global typedef 231 | 232 | 233 | ## AccessToken : String 234 | Regular JWT token. 235 | Its payload looks like this: 236 | ```js 237 | { 238 | "t": "Authomatic-AT", 239 | "uid": "userId", 240 | "exp": "someNumber", 241 | "jti": "randomBytes", 242 | ...otherClaims, 243 | "pld": { 244 | ...otherUserContent 245 | } 246 | } 247 | ``` 248 | 249 | **Kind**: global typedef 250 | 251 | 252 | ## RefreshToken : String 253 | regular JWT token. 254 | Its payload looks like this: 255 | ```js 256 | { 257 | "t": "Authomatic-RT", 258 | "iss": "Authomatic", 259 | "aud": ["Authomatic"] 260 | "uid": "userId", 261 | "exp": "someNumber", 262 | "jti": "randomBytes", 263 | "accessTokenJTI": "randomBytes" 264 | } 265 | ``` 266 | 267 | **Kind**: global typedef 268 | 269 | 270 | ## Tokens : Object 271 | Token pairs 272 | 273 | **Kind**: global typedef 274 | **Properties** 275 | 276 | | Name | Type | Description | 277 | | --- | --- | --- | 278 | | accessToken | [AccessToken](#AccessToken) | | 279 | | accessTokenExpiresAt | Number | epoch | 280 | | refreshToken | [RefreshToken](#RefreshToken) | | 281 | | refreshTokenExpiresAt | Number | epoch | 282 | 283 | 284 | 285 | ## VerifyOptions : Object 286 | Verify options to be used when verifying tokens 287 | 288 | **Kind**: global typedef 289 | **Properties** 290 | 291 | | Name | Type | Description | 292 | | --- | --- | --- | 293 | | [audience] | Array \| String | checks the aud field | 294 | | [issuer] | String \| Array | checks the iss field | 295 | | [ignoreExpiration] | Boolean | if true, ignores the expiration check of access tokens | 296 | | [ignoreNotBefore] | Boolean | if true, ignores the not before check of access tokens | 297 | | [subject] | String | checks the sub field | 298 | | [clockTolerance] | Number \| String | | 299 | | [maxAge] | String \| Number | | 300 | | [clockTimestamp] | Number | overrides the clock for the verification process | 301 | 302 | 303 | 304 | ## SignOptions : Object 305 | The allowed user options to for signing tokens 306 | 307 | **Kind**: global typedef 308 | **Properties** 309 | 310 | | Name | Type | 311 | | --- | --- | 312 | | [nbf] | Number | 313 | | [aud] | Array \| String | 314 | | [iss] | String | 315 | | [sub] | String | 316 | 317 | 318 | 319 | ## RefreshTokenNotFound : StandardError 320 | The refresh token was not found. 321 | 322 | **Kind**: global typedef 323 | **Properties** 324 | 325 | | Name | Type | Default | 326 | | --- | --- | --- | 327 | | [name] | String | 'RefreshTokenNotFound' | 328 | 329 | 330 | 331 | ## TokensMismatch : StandardError 332 | The tokens provided do not match 333 | 334 | **Kind**: global typedef 335 | **Properties** 336 | 337 | | Name | Type | Default | 338 | | --- | --- | --- | 339 | | [name] | String | 'TokensMismatch' | 340 | 341 | 342 | 343 | ## InvalidToken : StandardError 344 | The provided input is not a valid token. 345 | 346 | **Kind**: global typedef 347 | **Properties** 348 | 349 | | Name | Type | Default | 350 | | --- | --- | --- | 351 | | [name] | String | 'InvalidToken' | 352 | 353 | 354 | # Creating a store 355 | If you want to create a new store you need to expose the following functions: 356 | 357 | 1. add 358 | 359 | ```js 360 | /** 361 | * Register token and refresh token to the user 362 | * @param {String} userId 363 | * @param {String} refreshTokenJTI 364 | * @param {String} accessTokenJTI 365 | * @param {Number} ttl time to live in ms 366 | * @returns {Promise} returns true when created. 367 | */ 368 | function add(userId, refreshTokenJTI, accessTokenJTI, ttl){...} 369 | ``` 370 | 371 | 2. remove 372 | ```js 373 | /** 374 | * Remove a single refresh token from the user 375 | * @param userId 376 | * @param refreshTokenJTI 377 | * @returns {Promise} true if found and deleted, otherwise false. 378 | */ 379 | function remove(userId, refreshTokenJTI) {...} 380 | ``` 381 | 382 | 3. removeAll 383 | ```js 384 | /** 385 | * Removes all tokens for a particular user 386 | * @param userId 387 | * @returns {Promise} true if any were found and delete, false otherwise 388 | */ 389 | function removeAll(userId) {...} 390 | ``` 391 | You may need to expose a reference to the store if the user may need to handle connections during testing for example. 392 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | const util = require('util'); 5 | const jsonwebtoken = require('jsonwebtoken'); 6 | const t = require('tcomb'); 7 | const StandardError = require('standard-error'); 8 | 9 | const arrayOfStrings = t.refinement(t.Array, n => n.every(t.String), 'Array'); 10 | 11 | const Store = t.interface({ 12 | // Signature: (userId, refreshToken) 13 | remove: t.Function, 14 | // Signature: (userId) 15 | removeAll: t.Function, 16 | // Signature: (userId, refreshToken) 17 | add: t.Function 18 | }, 'Stores'); 19 | 20 | const JWT = t.interface({ 21 | // Signature: (payload, secret, {algorithm: String}) 22 | sign: t.Function, 23 | // Signature: (payload, secret, {algorithm: String, otherVerifyOptions}) 24 | verify: t.Function, 25 | // Signature: (payload) 26 | decode: t.Function, 27 | }, 'JWT'); 28 | 29 | /** 30 | * @type {String} 31 | * @typedef Secret 32 | * @description a string greater than 20 characters 33 | */ 34 | const Secret = secret => { 35 | t.String(secret); 36 | t.assert(secret.length >= 20, 'The secret must be greater than or equal 20 characters'); 37 | return secret; 38 | }; 39 | 40 | const ExpiresAt = t.Integer; 41 | const Algorithm = t.enums.of(['HS256', 'HS384', 'HS512', 'RS256'], 'Algorithm'); 42 | const UserId = t.String; 43 | const Prolong = t.Boolean; 44 | const accessTokenType = 'Authomatic-AT'; 45 | const refreshTokenType = 'Authomatic-RT'; 46 | // Does not add extra functionality to the token, merely makes it look complete and professional 47 | const refreshTokenSignOptions = { 48 | aud: ['Authomatic'], 49 | iss: 'Authomatic' 50 | }; 51 | const refreshTokenVerifyOptions = { 52 | audience: ['Authomatic'], 53 | issuer: 'Authomatic' 54 | }; 55 | 56 | /** 57 | * Access token 58 | * @typedef AccessToken 59 | * @type {String} 60 | * @description Regular JWT token. 61 | * Its payload looks like this: 62 | ```js 63 | { 64 | "t": "Authomatic-AT", 65 | "uid": "userId", 66 | "exp": "someNumber", 67 | "jti": "randomBytes", 68 | ...otherClaims, 69 | "pld": { 70 | ...otherUserContent 71 | } 72 | } 73 | ``` 74 | */ 75 | 76 | /** 77 | * Refresh token 78 | * @typedef RefreshToken 79 | * @type {String} 80 | * @description regular JWT token. 81 | * Its payload looks like this: 82 | ```js 83 | { 84 | "t": "Authomatic-RT", 85 | "iss": "Authomatic", 86 | "aud": ["Authomatic"] 87 | "uid": "userId", 88 | "exp": "someNumber", 89 | "jti": "randomBytes", 90 | "accessTokenJTI": "randomBytes" 91 | } 92 | ``` 93 | */ 94 | 95 | /** 96 | * Token pairs 97 | * @typedef Tokens 98 | * @type {Object} 99 | * @property {AccessToken} accessToken 100 | * @property {Number} accessTokenExpiresAt epoch 101 | * @property {RefreshToken} refreshToken 102 | * @property {Number} refreshTokenExpiresAt epoch 103 | */ 104 | 105 | /** 106 | * Verify options to be used when verifying tokens 107 | * @typedef VerifyOptions 108 | * @type {Object} 109 | * @property {Array|String} [audience] checks the aud field 110 | * @property {String|Array} [issuer] checks the iss field 111 | * @property {Boolean} [ignoreExpiration] if true, ignores the expiration check of access tokens 112 | * @property {Boolean} [ignoreNotBefore] if true, ignores the not before check of access tokens 113 | * @property {String} [subject] checks the sub field 114 | * @property {Number|String} [clockTolerance] 115 | * @property {String|Number} [maxAge] 116 | * @property {Number} [clockTimestamp] overrides the clock for the verification process 117 | */ 118 | const VerifyOptions = t.interface({ 119 | audience: t.maybe(t.union([arrayOfStrings, t.String])), 120 | issuer: t.maybe(t.union([t.String, arrayOfStrings])), 121 | ignoreExpiration: t.maybe(t.Boolean), 122 | ignoreNotBefore: t.maybe(t.Boolean), 123 | subject: t.maybe(t.String), 124 | clockTolerance: t.maybe(t.union([t.Number, t.String])), 125 | maxAge: t.maybe(t.union([t.String, t.Number])), 126 | clockTimestamp: t.maybe(t.Number) 127 | }, {name: 'VerifyOptions', strict: true}); 128 | 129 | /** 130 | * The allowed user options to for signing tokens 131 | * @typedef SignOptions 132 | * @type {Object} 133 | * @property {Number} [nbf] 134 | * @property {Array|String} [aud] 135 | * @property {String} [iss] 136 | * @property {String} [sub] 137 | */ 138 | const SignOptions = t.interface({ 139 | nbf: t.maybe(t.Number), 140 | aud: t.maybe(t.union([arrayOfStrings, t.String])), 141 | iss: t.maybe(t.String), 142 | sub: t.maybe(t.String), 143 | }, {name: 'SignOptions', strict: true}); 144 | 145 | const getTypeRefinement = tokenType => 146 | t.refinement( 147 | t.String, 148 | n => n === tokenType, 149 | `Token type: ${tokenType}` 150 | ); 151 | 152 | const internalSignOptions = SignOptions.extend(t.interface({ 153 | uid: UserId, 154 | jti: t.String, 155 | exp: ExpiresAt 156 | }, {name: 'InternalSignOptions', strict: true})); 157 | 158 | const Payload = internalSignOptions.extend(t.interface({ 159 | pld: t.Any, 160 | rme: t.Boolean, 161 | t: getTypeRefinement(accessTokenType) 162 | }, {name: 'Payload', strict: true})); 163 | 164 | const RefreshPayload = internalSignOptions.extend(t.interface({ 165 | accessTokenJTI: t.String, 166 | t: getTypeRefinement(refreshTokenType) 167 | }, {name: 'RefreshPayload', strict: true})); 168 | 169 | /** 170 | * The refresh token was not found. 171 | * @type {StandardError} 172 | * @typedef RefreshTokenNotFound 173 | * @property {String} [name='RefreshTokenNotFound'] 174 | */ 175 | const refreshTokenNotFound = new StandardError( 176 | 'The refresh token was not found', 177 | {name: 'RefreshTokenNotFound'} 178 | ); 179 | 180 | /** 181 | * The tokens provided do not match 182 | * @type {StandardError} 183 | * @typedef TokensMismatch 184 | * @property {String} [name='TokensMismatch'] 185 | */ 186 | const tokensMismatch = new StandardError( 187 | 'The tokens provided do not match', 188 | {name: 'TokensMismatch'} 189 | ); 190 | 191 | /** 192 | * The provided input is not a valid token. 193 | * @type {StandardError} 194 | * @typedef InvalidToken 195 | * @property {String} [name='InvalidToken'] 196 | */ 197 | const invalidToken = new StandardError( 198 | 'The provided input is not a valid token', 199 | {name: 'InvalidToken'} 200 | ); 201 | 202 | // 15 minutes 203 | const regularAccessTTL = 60 * 15; 204 | // 1 hour 205 | const prolongedAccessTTL = 60 * 60; 206 | // 25 minutes 207 | const regularRefreshTTL = 60 * 25; 208 | // 7 days 209 | const prolongedRefreshTTL = 60 * 60 * 24 * 7; 210 | 211 | // Seconds -> Seconds Since the Epoch 212 | const computeExpiryDate = seconds => Math.floor(Date.now() / 1000) + seconds; 213 | 214 | const randomBytes = util.promisify(crypto.randomBytes); 215 | 216 | const generateTokenId = () => randomBytes(32).then(x => x.toString('base64')); 217 | 218 | /** 219 | * Authomatic 220 | * @param {Object} store one of authomatic stores 221 | * @param {String} [algorithm=HS256] Can be one of these ['HS256', 'HS384', 'HS512', 'RS256'] 222 | * @param {SignOptions} [defaultSignOptions] 223 | * @param {VerifyOptions} [defaultVerifyOptions] 224 | */ 225 | module.exports = function Authomatic({ 226 | store, algorithm = 'HS256', 227 | jwt = jsonwebtoken, defaultSignOptions = {}, defaultVerifyOptions = {} 228 | }) { 229 | 230 | Store(store); 231 | Algorithm(algorithm); 232 | JWT(jwt); 233 | SignOptions(defaultSignOptions); 234 | VerifyOptions(defaultVerifyOptions); 235 | 236 | const checkToken = type => token => { 237 | const decodedATContent = jwt.decode(token); 238 | if(decodedATContent && decodedATContent.t === type) { 239 | return token; 240 | } 241 | throw invalidToken; 242 | }; 243 | 244 | const AccessToken = checkToken(accessTokenType); 245 | const RefreshToken = checkToken(refreshTokenType); 246 | 247 | const sign = async (userId, secret, content = {}, prolong = false, signOptions = {}) => { 248 | UserId(userId); 249 | Prolong(prolong); 250 | Secret(secret); 251 | 252 | const accessExp = computeExpiryDate(prolong ? prolongedAccessTTL : regularAccessTTL); 253 | const refreshTTL = prolong ? prolongedRefreshTTL : regularRefreshTTL; 254 | const refreshExp = computeExpiryDate(refreshTTL); 255 | 256 | // Order of spreading is important! 257 | const accessPayload = Payload({ 258 | ...defaultSignOptions, 259 | ...SignOptions(signOptions), 260 | pld: content, uid: userId, exp: accessExp, rme: prolong, 261 | jti: await generateTokenId(), t: accessTokenType 262 | }); 263 | 264 | const refreshPayload = RefreshPayload({ 265 | ...refreshTokenSignOptions, uid: userId, 266 | accessTokenJTI: accessPayload.jti, exp: refreshExp, 267 | jti: await generateTokenId(), t: refreshTokenType 268 | }); 269 | 270 | const accessToken = jwt.sign(accessPayload, secret, {algorithm}); 271 | const refreshToken = jwt.sign(refreshPayload, secret, {algorithm}); 272 | 273 | await store.add(userId, refreshPayload.jti, accessPayload.jti, refreshTTL * 1000); 274 | 275 | return { 276 | accessToken, accessTokenExpiresAt: accessExp, refreshToken, refreshTokenExpiresAt: refreshExp 277 | }; 278 | }; 279 | 280 | const verifyRefreshToken = (refreshToken, secret) => 281 | jwt.verify(RefreshToken(refreshToken), Secret(secret), { 282 | ...refreshTokenVerifyOptions, 283 | algorithm 284 | }); 285 | 286 | const verifyAccessToken = (token, secret, verifyOptions = {}) => 287 | jwt.verify(AccessToken(token), Secret(secret), { 288 | ...defaultVerifyOptions, 289 | ...VerifyOptions(verifyOptions), 290 | algorithm 291 | }); 292 | 293 | const refresh = async (refreshToken, accessToken, secret, verifyOptions) => { 294 | RefreshToken(refreshToken); 295 | // It is required to pass verifyOptions during refresh because the old function didn't have it 296 | VerifyOptions(verifyOptions); 297 | Secret(secret); 298 | 299 | const verifiedRTContent = verifyRefreshToken(refreshToken, secret); 300 | 301 | const {uid: userId, jti: refreshTokenJTI} = verifiedRTContent; 302 | 303 | // Eagerly invalidates refresh token 304 | if(!await store.remove(userId, refreshTokenJTI)) { 305 | throw refreshTokenNotFound; 306 | } 307 | 308 | AccessToken(accessToken); 309 | 310 | const verifiedATContent = 311 | verifyAccessToken(accessToken, secret, {...verifyOptions, ignoreExpiration: true}); 312 | 313 | // RefreshTokens works with only one AccessToken 314 | if (verifiedATContent.jti !== verifiedRTContent.accessTokenJTI) { 315 | throw tokensMismatch; 316 | } 317 | 318 | // eslint-disable-next-line no-unused-vars 319 | const {exp, iat, jti, uid, t, pld: payload, rme, ...jwtOptions} = verifiedATContent; 320 | 321 | // Finally, sign new tokens for the user 322 | return sign(userId, secret, payload, rme, jwtOptions); 323 | }; 324 | 325 | const invalidateRefreshToken = (refreshToken, secret) => { 326 | const {uid, jti} = verifyRefreshToken(refreshToken, secret); 327 | return store.remove(uid, jti); 328 | }; 329 | 330 | const invalidateAllRefreshTokens = userId => store.removeAll(UserId(userId)); 331 | 332 | return { 333 | /** 334 | * Returns access and refresh tokens 335 | * @param {String} userId 336 | * @param {Secret} secret 337 | * @param {Object} [content] user defined properties 338 | * @param {Boolean} [prolong] if true, the refreshToken will last 4 days and accessToken 1 hour, 339 | * otherwise the refresh token will last 25 minutes and the accessToken 15 minutes. 340 | * @param {SignOptions} [signOptions] Options to be passed to jwt.sign 341 | * @returns {Promise} 342 | * @throws {TypeError} typeError if any param was not sent exactly as specified 343 | */ 344 | sign, 345 | /** 346 | * Verifies token, might throw jwt.verify errors 347 | * @param {String} token 348 | * @param {Secret} secret 349 | * @param {VerifyOptions} [verifyOptions] Options to pass to jwt.verify. 350 | * @returns {String} decoded token 351 | * @throws {InvalidToken} invalidToken 352 | * @throws {TypeError} typeError if any param was not sent exactly as specified 353 | * @throws JsonWebTokenError 354 | * @throws TokenExpiredError 355 | * Error info at {@link https://www.npmjs.com/package/jsonwebtoken#errors--codes} 356 | */ 357 | verify: verifyAccessToken, 358 | /** 359 | * Issues a new access token using a refresh token and an old token (can be expired). 360 | * @param {String} refreshToken 361 | * @param {String} accessToken 362 | * @param {Secret} secret 363 | * @param {SignOptions} signOptions Options passed to jwt.sign, 364 | * ignoreExpiration will be set to true 365 | * @returns {Promise} 366 | * @throws {RefreshTokenNotFound} refreshTokenNotFound 367 | * @throws {TokensMismatch} tokensMismatch 368 | * @throws {TypeError} typeError if any param was not sent exactly as specified 369 | * @throws JsonWebTokenError 370 | * @throws TokenExpiredError 371 | * Error info at {@link https://www.npmjs.com/package/jsonwebtoken#errors--codes} 372 | */ 373 | refresh, 374 | /** 375 | * Invalidates refresh token 376 | * @param {String} refreshToken 377 | * @returns {Promise} true if successful, false otherwise. 378 | * @throws {TypeError} typeError if any param was not sent exactly as specified 379 | * @throws {InvalidToken} invalidToken 380 | * @throws JsonWebTokenError 381 | * @throws TokenExpiredError 382 | * Error info at {@link https://www.npmjs.com/package/jsonwebtoken#errors--codes} 383 | */ 384 | invalidateRefreshToken, 385 | /** 386 | * Invalidates all refresh tokens 387 | * @param {String} userId 388 | * @returns {Promise} true if successful, false otherwise. 389 | * @throws {TypeError} typeError if any param was not sent exactly as specified 390 | */ 391 | invalidateAllRefreshTokens 392 | }; 393 | }; 394 | --------------------------------------------------------------------------------