From 58b1a72ca6a6d5afbb64746721bbae0165f5627d Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 11:33:59 +0200 Subject: [PATCH 1/9] Added Token Manager --- packages/server/tsconfig.json | 10 +- packages/token-manager/__tests__/index.ts | 7 + .../token-manager/__tests__/token-manager.ts | 79 ++++++++ packages/token-manager/package.json | 37 ++++ packages/token-manager/src/index.ts | 1 + packages/token-manager/src/token-manager.ts | 67 +++++++ .../token-manager/src/types/configuration.ts | 13 ++ .../types/token-generation-configuration.ts | 25 +++ packages/token-manager/tsconfig.json | 9 + packages/token-manager/yarn.lock | 179 ++++++++++++++++++ 10 files changed, 419 insertions(+), 8 deletions(-) create mode 100644 packages/token-manager/__tests__/index.ts create mode 100644 packages/token-manager/__tests__/token-manager.ts create mode 100644 packages/token-manager/package.json create mode 100644 packages/token-manager/src/index.ts create mode 100644 packages/token-manager/src/token-manager.ts create mode 100644 packages/token-manager/src/types/configuration.ts create mode 100644 packages/token-manager/src/types/token-generation-configuration.ts create mode 100644 packages/token-manager/tsconfig.json create mode 100644 packages/token-manager/yarn.lock diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 7a3de3c2a..f34cff118 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -3,13 +3,7 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./lib", - "typeRoots": [ - "node_modules/@types" - ] + "typeRoots": ["node_modules/@types"] }, - "exclude": [ - "node_modules", - "__tests__", - "lib" - ] + "exclude": ["node_modules", "__tests__", "lib"] } diff --git a/packages/token-manager/__tests__/index.ts b/packages/token-manager/__tests__/index.ts new file mode 100644 index 000000000..4f146aaad --- /dev/null +++ b/packages/token-manager/__tests__/index.ts @@ -0,0 +1,7 @@ +import TokenManager from '../src'; + +describe('TokenManager entry', () => { + it('should have default export TokenManager', () => { + expect(typeof TokenManager).toBe('function'); + }); +}); diff --git a/packages/token-manager/__tests__/token-manager.ts b/packages/token-manager/__tests__/token-manager.ts new file mode 100644 index 000000000..9b5beb6ae --- /dev/null +++ b/packages/token-manager/__tests__/token-manager.ts @@ -0,0 +1,79 @@ +import TokenManager from '../src'; + +const TM = new TokenManager({ + secret: 'test', + emailTokenExpiration: 60_000, +}); + +describe('TokenManager', () => { + describe('validateConfiguration', () => { + it('should throw if no configuration provided', () => { + expect(() => new TokenManager()).toThrow(); + }); + + it('should throw if configuration does not provide secret property', () => { + expect(() => new TokenManager({})).toThrow(); + }); + }); + + describe('generateRandomToken', () => { + it('should return a 86 char (43 bytes to hex) long random string when no parameters provided', () => { + expect(typeof TM.generateRandomToken()).toBe('string'); + expect(TM.generateRandomToken().length).toBe(86); + }); + + it('should return random string with the first parameter as length', () => { + expect(typeof TM.generateRandomToken(10)).toBe('string'); + expect(TM.generateRandomToken(10).length).toBe(20); + }); + }); + + describe('generateAccessToken', () => { + it('should return a string when first parameter provided', () => { + expect(typeof TM.generateAccessToken({ sessionId: 'test' })).toBe('string'); + }); + }); + + describe('generateRefreshToken', () => { + it('should return a string', () => { + expect(typeof TM.generateRefreshToken()).toBe('string'); + expect(typeof TM.generateRefreshToken({ sessionId: 'test' })).toBe('string'); + }); + }); + + describe('isEmailTokenExpired', () => { + it('should return true if the token provided is expired', () => { + const token = ''; + const tokenRecord = { when: 0 }; + expect(TM.isEmailTokenExpired(token, tokenRecord)).toBe(true); + }); + + it('should return false if the token provided is not expired', () => { + const token = ''; + const tokenRecord = { when: Date.now() + 100_000 }; + expect(TM.isEmailTokenExpired(token, tokenRecord)).toBe(false); + }); + }); + + describe('decodeToken', () => { + const TMdecode = new TokenManager({ + secret: 'test', + access: { + expiresIn: '0s', + }, + }); + + it('should not ignore expiration by default', () => { + const tokenData = { user: 'test' }; + const token = TMdecode.generateAccessToken(tokenData); + expect(() => { + TMdecode.decodeToken(token); + }).toThrow(); + }); + it('should return the decoded token anyway when ignoreExpiration is true', () => { + const tokenData = { user: 'test' }; + const token = TMdecode.generateAccessToken(tokenData); + expect(TMdecode.decodeToken(token, true).data.user).toBe('test'); + }); + }); +}); diff --git a/packages/token-manager/package.json b/packages/token-manager/package.json new file mode 100644 index 000000000..924267ccb --- /dev/null +++ b/packages/token-manager/package.json @@ -0,0 +1,37 @@ +{ + "name": "@accounts/token-manager", + "version": "0.1.0-beta.10", + "license": "MIT", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "clean": "rimraf lib", + "start": "tsc --watch", + "precompile": "npm run clean", + "compile": "tsc", + "prepublishOnly": "npm run compile", + "test": "npm run testonly", + "test-ci": "npm lint && npm coverage", + "testonly": "jest", + "test:watch": "jest --watch", + "coverage": "npm run testonly -- --coverage" + }, + "jest": { + "transform": { + ".(ts|tsx)": "/../../node_modules/ts-jest/preprocessor.js" + }, + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", + "moduleFileExtensions": ["ts", "js"] + }, + "dependencies": { + "bcryptjs": "^2.4.0", + "jsonwebtoken": "^8.0.0", + "jwt-decode": "^2.1.0", + "@accounts/types": "^0.1.0-beta.10" + }, + "devDependencies": { + "@types/jsonwebtoken": "7.2.5", + "@types/jwt-decode": "2.2.1", + "rimraf": "2.6.2" + } +} diff --git a/packages/token-manager/src/index.ts b/packages/token-manager/src/index.ts new file mode 100644 index 000000000..aa5953763 --- /dev/null +++ b/packages/token-manager/src/index.ts @@ -0,0 +1 @@ +export { default } from './token-manager'; \ No newline at end of file diff --git a/packages/token-manager/src/token-manager.ts b/packages/token-manager/src/token-manager.ts new file mode 100644 index 000000000..091ef995f --- /dev/null +++ b/packages/token-manager/src/token-manager.ts @@ -0,0 +1,67 @@ +import { TokenRecord } from '@accounts/types'; + +import { randomBytes } from 'crypto'; +import * as jwt from 'jsonwebtoken'; + +import { Configuration } from './types/configuration'; +import { TokenGenerationConfiguration } from './types/token-generation-configuration'; + +const defaultTokenConfig: TokenGenerationConfiguration = { + algorithm: 'HS256', +}; + +const defaultAccessTokenConfig: TokenGenerationConfiguration = { + expiresIn: '90m', +}; + +const defaultRefreshTokenConfig: TokenGenerationConfiguration = { + expiresIn: '7d', +}; + +export default class TokenManager { + + private secret: string; + + private emailTokenExpiration: number; + + private accessTokenConfig: TokenGenerationConfiguration; + + private refreshTokenConfig: TokenGenerationConfiguration; + + constructor(config: Configuration) { + this.validateConfiguration(config); + this.secret = config.secret; + this.emailTokenExpiration = config.emailTokenExpiration || 1000 * 60; + this.accessTokenConfig = { ...defaultTokenConfig, ...defaultAccessTokenConfig, ...config.access }; + this.refreshTokenConfig = { ...defaultTokenConfig, ...defaultRefreshTokenConfig, ...config.refresh }; + } + + public generateRandomToken(length: number | undefined): string { + return randomBytes(length || 43).toString('hex'); + } + + public generateAccessToken(data): string { + return jwt.sign({ data }, this.secret, this.accessTokenConfig); + } + + public generateRefreshToken(data = {}): string { + return jwt.sign({ data }, this.secret, this.refreshTokenConfig); + } + + public isEmailTokenExpired(token: string, tokenRecord?: TokenRecord): boolean { + return !tokenRecord || Number(tokenRecord.when) + this.emailTokenExpiration < Date.now(); + } + + public decodeToken(token: string, ignoreExpiration: boolean = false): string | object { + return jwt.verify(token, this.secret, { ignoreExpiration }); + } + + private validateConfiguration(config: Configuration): void { + if (!config) { + throw new Error('[ Accounts - TokenManager ] configuration : A configuration object is needed'); + } + if (typeof config.secret !== 'string') { + throw new Error('[ Accounts - TokenManager ] configuration : A string secret property is needed'); + } + } +} diff --git a/packages/token-manager/src/types/configuration.ts b/packages/token-manager/src/types/configuration.ts new file mode 100644 index 000000000..d0ee344e4 --- /dev/null +++ b/packages/token-manager/src/types/configuration.ts @@ -0,0 +1,13 @@ +import { TokenGenerationConfiguration } from './token-generation-configuration'; + +export interface Configuration { + + secret: string; + + emailTokenExpiration?: number; + + access?: TokenGenerationConfiguration; + + refresh?: TokenGenerationConfiguration; + +} diff --git a/packages/token-manager/src/types/token-generation-configuration.ts b/packages/token-manager/src/types/token-generation-configuration.ts new file mode 100644 index 000000000..3edabcba1 --- /dev/null +++ b/packages/token-manager/src/types/token-generation-configuration.ts @@ -0,0 +1,25 @@ +export interface TokenGenerationConfiguration { + + algorithm?: string; + + expiresIn?: string; + + // TODO : explore jwt configuration + /* + + notBefore?: string; + + audience?: string | string[] | RegExp | RegExp[]; + + To complete + + jwtid: + + subject:null, + + noTimestamp:null, + + header:null, + + keyid:null,*/ +} diff --git a/packages/token-manager/tsconfig.json b/packages/token-manager/tsconfig.json new file mode 100644 index 000000000..f34cff118 --- /dev/null +++ b/packages/token-manager/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "typeRoots": ["node_modules/@types"] + }, + "exclude": ["node_modules", "__tests__", "lib"] +} diff --git a/packages/token-manager/yarn.lock b/packages/token-manager/yarn.lock new file mode 100644 index 000000000..515f6b049 --- /dev/null +++ b/packages/token-manager/yarn.lock @@ -0,0 +1,179 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/jsonwebtoken@7.2.5": + version "7.2.5" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.5.tgz#413159d570ec45fc3e7da7ea3f7bca6fb9300e72" + dependencies: + "@types/node" "*" + +"@types/jwt-decode@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2" + +"@types/node@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.1.tgz#e2d374ef15b315b48e7efc308fa1a7cd51faa06c" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64url@2.0.0, base64url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" + +bcryptjs@^2.4.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +ecdsa-sig-formatter@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" + dependencies: + base64url "^2.0.0" + safe-buffer "^5.0.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@^7.0.5: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +jsonwebtoken@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz#690ec3a9e7e95e2884347ce3e9eb9d389aa598b3" + dependencies: + jws "^3.1.4" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + xtend "^4.0.1" + +jwa@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" + dependencies: + base64url "2.0.0" + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.9" + safe-buffer "^5.0.1" + +jws@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" + dependencies: + base64url "^2.0.0" + jwa "^1.1.4" + safe-buffer "^5.0.1" + +jwt-decode@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +rimraf@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +safe-buffer@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xtend@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From 538869d3b62bd6298a504e55da36b9adcb111f69 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 11:52:18 +0200 Subject: [PATCH 2/9] Integration with accountsServer: constructor --- .../__snapshots__/account-server.ts.snap | 2 + packages/server/__tests__/account-server.ts | 71 +++++++++++++++++-- packages/server/package.json | 3 +- packages/server/src/accounts-server.ts | 5 ++ .../src/types/accounts-server-options.ts | 6 +- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/server/__tests__/__snapshots__/account-server.ts.snap b/packages/server/__tests__/__snapshots__/account-server.ts.snap index 4fbde10aa..ca8c2a1e3 100644 --- a/packages/server/__tests__/__snapshots__/account-server.ts.snap +++ b/packages/server/__tests__/__snapshots__/account-server.ts.snap @@ -2,6 +2,8 @@ exports[`AccountsServer config throws on invalid db 1`] = `"A database driver is required"`; +exports[`AccountsServer config throws on invalid tokenManager 1`] = `"A tokenManager is required"`; + exports[`AccountsServer loginWithService throws on invalid service 1`] = `"No service with the name facebook was registered."`; exports[`AccountsServer loginWithService throws when user not found 1`] = `"Service facebook was not able to authenticate user"`; diff --git a/packages/server/__tests__/account-server.ts b/packages/server/__tests__/account-server.ts index 6178c504f..208ac62d9 100644 --- a/packages/server/__tests__/account-server.ts +++ b/packages/server/__tests__/account-server.ts @@ -3,6 +3,11 @@ import { AccountsServer } from '../src/accounts-server'; import { JwtData } from '../src/types/jwt-data'; import { bcryptPassword, hashPassword, verifyPassword } from '../src/utils/encryption'; import { ServerHooks } from '../src/utils/server-hooks'; +import TokenManager from '@accounts/token-manager'; + +const tokenManager = new TokenManager({ + secret: 'secret', +}); describe('AccountsServer', () => { const db = { @@ -23,6 +28,17 @@ describe('AccountsServer', () => { }); }); + describe('config', () => { + it('throws on invalid tokenManager', async () => { + try { + const account = new AccountsServer({ db: {} } as any, {}); + throw new Error(); + } catch (err) { + expect(err.message).toMatchSnapshot(); + } + }); + }); + describe('getServices', () => { it('should return instance services', async () => { const services: any = { @@ -30,7 +46,7 @@ describe('AccountsServer', () => { setStore: () => null, }, }; - const account = new AccountsServer({ db: {} } as any, services); + const account = new AccountsServer({ db: {}, tokenManager } as any, services); expect(account.getServices()).toEqual(services); }); }); @@ -38,7 +54,7 @@ describe('AccountsServer', () => { describe('loginWithService', () => { it('throws on invalid service', async () => { try { - const accountServer = new AccountsServer({ db: {} } as any, {}); + const accountServer = new AccountsServer({ db: {}, tokenManager } as any, {}); await accountServer.loginWithService('facebook', {}, {}); throw new Error(); } catch (err) { @@ -49,7 +65,7 @@ describe('AccountsServer', () => { it('throws when user not found', async () => { const authenticate = jest.fn(() => Promise.resolve()); try { - const accountServer = new AccountsServer({ db: {} } as any, { + const accountServer = new AccountsServer({ db: {}, tokenManager } as any, { facebook: { authenticate, setStore: jest.fn() }, }); await accountServer.loginWithService('facebook', {}, {}); @@ -66,6 +82,7 @@ describe('AccountsServer', () => { { db: { createSession } as any, tokenSecret: 'secret', + tokenManager, }, { facebook: { authenticate, setStore: jest.fn() }, @@ -93,13 +110,16 @@ describe('AccountsServer', () => { createSession: () => Promise.resolve('sessionId'), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); const res = await accountsServer.loginWithUser(user, {}); const { accessToken, refreshToken } = res.tokens; - const decodedAccessToken: { data: JwtData } = jwtDecode(accessToken); + const decodedAccessToken: { data: JwtData } = accountsServer.tokenManager.decodeToken( + accessToken + ); expect(decodedAccessToken.data.token).toBeTruthy(); expect(accessToken).toBeTruthy(); expect(refreshToken).toBeTruthy(); @@ -120,6 +140,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -153,6 +174,7 @@ describe('AccountsServer', () => { invalidateSession, } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -172,6 +194,7 @@ describe('AccountsServer', () => { createSession: () => '123', } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -191,6 +214,7 @@ describe('AccountsServer', () => { }, } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -225,6 +249,7 @@ describe('AccountsServer', () => { invalidateSession, } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -249,6 +274,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -283,6 +309,7 @@ describe('AccountsServer', () => { resumeSessionValidator: () => Promise.resolve(user), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -312,6 +339,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(user), } as any, tokenSecret: 'secret', + tokenManager, resumeSessionValidator: () => Promise.resolve(user), }, {} @@ -342,6 +370,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(user), } as any, tokenSecret: 'secret', + tokenManager, resumeSessionValidator: () => Promise.resolve(user), }, {} @@ -370,6 +399,7 @@ describe('AccountsServer', () => { }), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -404,6 +434,7 @@ describe('AccountsServer', () => { updateSession: () => Promise.resolve(), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -426,6 +457,7 @@ describe('AccountsServer', () => { { db: db as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -452,6 +484,7 @@ describe('AccountsServer', () => { createSession: () => Promise.resolve('001'), } as any, tokenSecret: 'secret', + tokenManager, impersonationAuthorize: async (userObject, impersonateToUser) => { return userObject.id === user.id && impersonateToUser === impersonatedUser; }, @@ -499,6 +532,7 @@ describe('AccountsServer', () => { updateSession, } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -520,6 +554,7 @@ describe('AccountsServer', () => { { db: {} as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -535,6 +570,7 @@ describe('AccountsServer', () => { { db: {} as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -553,6 +589,7 @@ describe('AccountsServer', () => { findSessionByToken: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -575,6 +612,7 @@ describe('AccountsServer', () => { }), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -600,6 +638,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -619,6 +658,7 @@ describe('AccountsServer', () => { { db: {} as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -634,6 +674,7 @@ describe('AccountsServer', () => { { db: {} as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -651,6 +692,7 @@ describe('AccountsServer', () => { findSessionByToken: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -672,6 +714,7 @@ describe('AccountsServer', () => { }), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -692,6 +735,7 @@ describe('AccountsServer', () => { { db: { findUserById } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -715,6 +759,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -746,6 +791,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(user), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -771,6 +817,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(user), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -789,6 +836,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -817,6 +865,7 @@ describe('AccountsServer', () => { setProfile, } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -850,6 +899,7 @@ describe('AccountsServer', () => { setProfile, } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -872,6 +922,7 @@ describe('AccountsServer', () => { { db: db as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -888,6 +939,7 @@ describe('AccountsServer', () => { { db: {} as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -905,6 +957,7 @@ describe('AccountsServer', () => { { db: {} as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -931,6 +984,7 @@ describe('AccountsServer', () => { findUserById: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -960,6 +1014,7 @@ describe('AccountsServer', () => { findUserByUsername: () => Promise.resolve(null), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -987,6 +1042,7 @@ describe('AccountsServer', () => { findUserByUsername: () => Promise.resolve(someUser), } as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -1010,6 +1066,8 @@ describe('AccountsServer', () => { findUserByUsername: () => Promise.resolve(someUser), } as any, tokenSecret: 'secret', + tokenManager, + impersonationAuthorize: async (userObject, impersonateToUser) => { return userObject.id === user.id && impersonateToUser === impersonatedUser; }, @@ -1042,6 +1100,8 @@ describe('AccountsServer', () => { createSession, } as any, tokenSecret: 'secret', + tokenManager, + impersonationAuthorize: async (userObject, impersonateToUser) => { return userObject.id === user.id && impersonateToUser === impersonatedUser; }, @@ -1081,6 +1141,7 @@ describe('AccountsServer', () => { { db: db as any, tokenSecret: 'secret', + tokenManager, }, {} ); @@ -1093,6 +1154,8 @@ describe('AccountsServer', () => { { db: db as any, tokenSecret: 'secret', + tokenManager, + userObjectSanitizer: (user, omit) => omit(user, ['username']), }, {} diff --git a/packages/server/package.json b/packages/server/package.json index 3170dadf4..cb20e98fd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -58,6 +58,7 @@ "devDependencies": { "@types/jsonwebtoken": "7.2.6", "@types/jwt-decode": "2.2.1", - "rimraf": "2.6.2" + "rimraf": "2.6.2", + "@accounts/token-manager": "^0.1.0-beta.10" } } diff --git a/packages/server/src/accounts-server.ts b/packages/server/src/accounts-server.ts index be86b7247..42ca59036 100644 --- a/packages/server/src/accounts-server.ts +++ b/packages/server/src/accounts-server.ts @@ -44,6 +44,7 @@ const defaultOptions = { export class AccountsServer { public options: AccountsServerOptions; + public tokenManager: any; private services: { [key: string]: AuthenticationService }; private db: DatabaseInterface; private hooks: Emittery; @@ -53,10 +54,14 @@ export class AccountsServer { if (!this.options.db) { throw new AccountsError('A database driver is required'); } + if (!this.options.tokenManager) { + throw new AccountsError('A tokenManager is required'); + } // TODO if this.options.tokenSecret === 'secret' warm user to change it this.services = services; this.db = this.options.db; + this.tokenManager = this.options.tokenManager; // Set the db to all services // tslint:disable-next-line diff --git a/packages/server/src/types/accounts-server-options.ts b/packages/server/src/types/accounts-server-options.ts index dd1b5678b..eaa3895f0 100644 --- a/packages/server/src/types/accounts-server-options.ts +++ b/packages/server/src/types/accounts-server-options.ts @@ -8,6 +8,7 @@ import { SendMailType } from './send-mail-type'; export interface AccountsServerOptions { db: DatabaseInterface; + tokenManager: any; tokenSecret: string; tokenConfigs?: { accessToken?: { @@ -20,10 +21,7 @@ export interface AccountsServerOptions { emailTokensExpiry?: number; emailTemplates?: EmailTemplatesType; userObjectSanitizer?: UserObjectSanitizerFunction; - impersonationAuthorize?: ( - user: User, - impersonateToUser: User - ) => Promise; + impersonationAuthorize?: (user: User, impersonateToUser: User) => Promise; resumeSessionValidator?: ResumeSessionValidator; siteUrl?: string; prepareMail?: PrepareMailFunction; From 804d71767508e2cacd3cd85fd4998bb48451fda7 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 11:54:29 +0200 Subject: [PATCH 3/9] Accounts Server uses TokenManager --- packages/server/src/accounts-server.ts | 35 +++++++++----------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/server/src/accounts-server.ts b/packages/server/src/accounts-server.ts index 42ca59036..ad42f18b5 100644 --- a/packages/server/src/accounts-server.ts +++ b/packages/server/src/accounts-server.ts @@ -125,7 +125,7 @@ export class AccountsServer { const { ip, userAgent } = infos; try { - const token = generateRandomToken(); + const token = this.tokenManager.generateRandomToken(); const sessionId = await this.db.createSession(user.id, token, { ip, userAgent, @@ -173,7 +173,7 @@ export class AccountsServer { } try { - jwt.verify(accessToken, this.options.tokenSecret); + this.tokenManager.decodeToken(accessToken); } catch (err) { throw new AccountsError('Access token is not valid'); } @@ -213,7 +213,7 @@ export class AccountsServer { return { authorized: false }; } - const token = generateRandomToken(); + const token = this.tokenManager.generateRandomToken(); const newSessionId = await this.db.createSession( impersonatedUser.id, token, @@ -264,10 +264,11 @@ export class AccountsServer { let sessionToken: string; try { - jwt.verify(refreshToken, this.options.tokenSecret); - const decodedAccessToken = jwt.verify(accessToken, this.options.tokenSecret, { - ignoreExpiration: true, - }) as { data: JwtData }; + this.tokenManager.decodeToken(refreshToken); + const decodedAccessToken: { data: JwtData } = this.tokenManager.decodeToken( + accessToken, + true + ); sessionToken = decodedAccessToken.data.token; } catch (err) { throw new AccountsError('Tokens are not valid'); @@ -315,19 +316,9 @@ export class AccountsServer { */ public createTokens(token: string, isImpersonated: boolean = false): Tokens { const { tokenSecret, tokenConfigs } = this.options; - const jwtData: JwtData = { - token, - isImpersonated, - }; - const accessToken = generateAccessToken({ - data: jwtData, - secret: tokenSecret, - config: tokenConfigs.accessToken || {}, - }); - const refreshToken = generateRefreshToken({ - secret: tokenSecret, - config: tokenConfigs.refreshToken || {}, - }); + const jwtData: JwtData = { token, isImpersonated }; + const accessToken = this.tokenManager.generateAccessToken(jwtData); + const refreshToken = this.tokenManager.generateRefreshToken(); return { accessToken, refreshToken }; } @@ -414,9 +405,7 @@ export class AccountsServer { let sessionToken: string; try { - const decodedAccessToken = jwt.verify(accessToken, this.options.tokenSecret) as { - data: JwtData; - }; + const decodedAccessToken: { data: JwtData } = this.tokenManager.decodeToken(accessToken); sessionToken = decodedAccessToken.data.token; } catch (err) { throw new AccountsError('Tokens are not valid'); From 038a64fb544aa253905296b9f04ee3ece15094d6 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 11:58:52 +0200 Subject: [PATCH 4/9] Accounts Password uses TokenManager --- packages/password/src/accounts-password.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/password/src/accounts-password.ts b/packages/password/src/accounts-password.ts index 40349811b..c90b34d28 100644 --- a/packages/password/src/accounts-password.ts +++ b/packages/password/src/accounts-password.ts @@ -2,7 +2,7 @@ import { trim, isEmpty, isFunction, isString, isPlainObject, get, find, includes import { CreateUser, User, Login, EmailRecord, TokenRecord, DatabaseInterface, AuthenticationService } from '@accounts/types'; import { HashAlgorithm } from '@accounts/common'; import { TwoFactor, AccountsTwoFactorOptions } from '@accounts/two-factor'; -import { AccountsServer, generateRandomToken, getFirstUserEmail } from '@accounts/server'; +import { AccountsServer, getFirstUserEmail } from '@accounts/server'; import { hashPassword, bcryptPassword, verifyPassword } from './utils/encryption'; import { PasswordCreateUserType } from './types/password-create-user-type'; @@ -165,7 +165,7 @@ export default class AccountsPassword implements AuthenticationService { const resetTokens = get(user, ['services', 'password', 'reset']); const resetTokenRecord = find(resetTokens, t => t.token === token); - if (this.server.isTokenExpired(token, resetTokenRecord)) { + if (this.server.tokenManager.isEmailTokenExpired(token, resetTokenRecord)) { throw new Error('Reset password link expired'); } @@ -212,7 +212,7 @@ export default class AccountsPassword implements AuthenticationService { if (!address || !includes(emails.map(email => email.address), address)) { throw new Error('No such email address for user'); } - const token = generateRandomToken(); + const token = this.server.tokenManager.generateRandomToken(); await this.db.addEmailVerificationToken(user.id, address, token); const resetPasswordMail = this.server.prepareMail( @@ -243,7 +243,7 @@ export default class AccountsPassword implements AuthenticationService { throw new Error('User not found'); } address = getFirstUserEmail(user, address); - const token = generateRandomToken(); + const token = this.server.tokenManager.generateRandomToken(); await this.db.addResetPasswordToken(user.id, address, token); const resetPasswordMail = this.server.prepareMail( @@ -271,7 +271,7 @@ export default class AccountsPassword implements AuthenticationService { throw new Error('User not found'); } address = getFirstUserEmail(user, address); - const token = generateRandomToken(); + const token = this.server.tokenManager.generateRandomToken(); await this.db.addResetPasswordToken(user.id, address, token, 'enroll'); const enrollmentMail = this.server.prepareMail( From 2470c93a9af4fde1b4515d7d0a7e93b37af618b2 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 12:20:17 +0200 Subject: [PATCH 5/9] Fix accounts password tests --- .../password/__tests__/accounts-password.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/password/__tests__/accounts-password.ts b/packages/password/__tests__/accounts-password.ts index 588bdec35..9edf57f4c 100644 --- a/packages/password/__tests__/accounts-password.ts +++ b/packages/password/__tests__/accounts-password.ts @@ -204,9 +204,9 @@ describe('AccountsPassword', () => { it('throws when token is expired', async () => { const findUserByResetPasswordToken = jest.fn(() => Promise.resolve(invalidUser)); - const isTokenExpired = jest.fn(() => true); + const isEmailTokenExpired = jest.fn(() => true); password.setStore({ findUserByResetPasswordToken } as any); - password.server = { isTokenExpired } as any; + password.server = { tokenManager: { isEmailTokenExpired } } as any; try { await password.resetPassword(token, newPassword); throw new Error(); @@ -217,9 +217,9 @@ describe('AccountsPassword', () => { it('throws when token have invalid email', async () => { const findUserByResetPasswordToken = jest.fn(() => Promise.resolve(invalidUser)); - const isTokenExpired = jest.fn(() => false); + const isEmailTokenExpired = jest.fn(() => false); password.setStore({ findUserByResetPasswordToken } as any); - password.server = { isTokenExpired } as any; + password.server = { tokenManager: { isEmailTokenExpired } } as any; try { await password.resetPassword(token, newPassword); throw new Error(); @@ -230,7 +230,7 @@ describe('AccountsPassword', () => { it('reset password and invalidate all sessions', async () => { const findUserByResetPasswordToken = jest.fn(() => Promise.resolve(validUser)); - const isTokenExpired = jest.fn(() => false); + const isEmailTokenExpired = jest.fn(() => false); const setResetPassword = jest.fn(() => Promise.resolve()); const invalidateAllSessions = jest.fn(() => Promise.resolve()); password.setStore({ @@ -238,7 +238,7 @@ describe('AccountsPassword', () => { setResetPassword, invalidateAllSessions, } as any); - password.server = { isTokenExpired } as any; + password.server = { tokenManager: { isEmailTokenExpired } } as any; await password.resetPassword(token, newPassword); expect(setResetPassword.mock.calls.length).toBe(1); expect(invalidateAllSessions.mock.calls[0]).toMatchSnapshot(); @@ -307,6 +307,9 @@ describe('AccountsPassword', () => { prepareMail, options: { sendMail }, sanitizeUser, + tokenManager: { + generateRandomToken: () => 'randomToken' + } } as any; set(password.server, 'options.emailTemplates', {}); await password.sendVerificationEmail(verifiedEmail); @@ -326,6 +329,9 @@ describe('AccountsPassword', () => { prepareMail, options: { sendMail }, sanitizeUser, + tokenManager: { + generateRandomToken: () => 'randomToken' + } } as any; set(password.server, 'options.emailTemplates', {}); await password.sendVerificationEmail(email); @@ -372,6 +378,9 @@ describe('AccountsPassword', () => { options: { sendMail }, sanitizeUser, getFirstUserEmail, + tokenManager: { + generateRandomToken: () => 'randomToken' + } } as any; set(password.server, 'options.emailTemplates', {}); await password.sendResetPasswordEmail(email); @@ -409,6 +418,9 @@ describe('AccountsPassword', () => { options: { sendMail }, sanitizeUser, getFirstUserEmail, + tokenManager: { + generateRandomToken: () => 'randomToken' + } } as any; set(password.server, 'options.emailTemplates', {}); await password.sendEnrollmentEmail(email); From 56ddb7f19b076a79e6dd8618e6b6545fac063cae Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 12:21:54 +0200 Subject: [PATCH 6/9] Removed useless code :D --- packages/server/__tests__/account-server.ts | 3 -- packages/server/__tests__/utils/encryption.ts | 33 --------------- packages/server/package.json | 5 --- packages/server/src/accounts-server.ts | 3 -- packages/server/src/index.ts | 4 -- packages/server/src/utils/encryption.ts | 23 ---------- packages/server/src/utils/tokens.ts | 42 ------------------- 7 files changed, 113 deletions(-) delete mode 100644 packages/server/__tests__/utils/encryption.ts delete mode 100644 packages/server/src/utils/encryption.ts delete mode 100644 packages/server/src/utils/tokens.ts diff --git a/packages/server/__tests__/account-server.ts b/packages/server/__tests__/account-server.ts index 208ac62d9..72aa0890d 100644 --- a/packages/server/__tests__/account-server.ts +++ b/packages/server/__tests__/account-server.ts @@ -1,7 +1,5 @@ -import * as jwtDecode from 'jwt-decode'; import { AccountsServer } from '../src/accounts-server'; import { JwtData } from '../src/types/jwt-data'; -import { bcryptPassword, hashPassword, verifyPassword } from '../src/utils/encryption'; import { ServerHooks } from '../src/utils/server-hooks'; import TokenManager from '@accounts/token-manager'; @@ -95,7 +93,6 @@ describe('AccountsServer', () => { describe('loginWithUser', () => { it('creates a session when given a proper user object', async () => { - const hash = bcryptPassword('1234567'); const user = { id: '123', username: 'username', diff --git a/packages/server/__tests__/utils/encryption.ts b/packages/server/__tests__/utils/encryption.ts deleted file mode 100644 index b365b7513..000000000 --- a/packages/server/__tests__/utils/encryption.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - bcryptPassword, - hashPassword, - verifyPassword, -} from '../../src/utils/encryption'; - -describe('bcryptPassword', () => { - it('hashes password using bcrypt', async () => { - const hash = await bcryptPassword('password'); - expect(hash).toBeTruthy(); - }); -}); - -describe('hashPassword', () => { - it('hashes password', async () => { - const hash = await hashPassword('password', 'sha256'); - expect(hash).toBe('5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'); - }); -}); - -describe('verifyPassword', () => { - it('true if password matches', async () => { - const password = 'password'; - const hash = await bcryptPassword(password); - expect(await verifyPassword(password, hash)).toBe(true); - }); - it('false if password does not match', async () => { - const password = 'password'; - const wrongPassword = 'wrongPassword'; - const hash = await bcryptPassword(password); - expect(await verifyPassword(wrongPassword, hash)).toBe(false); - }); -}); diff --git a/packages/server/package.json b/packages/server/package.json index cb20e98fd..e0d9fe5f7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -49,15 +49,10 @@ "@accounts/common": "^0.1.0-beta.10", "@accounts/types": "^0.1.0-beta.10", "babel-polyfill": "^6.23.0", - "bcryptjs": "^2.4.0", "emittery": "^0.3.0", - "jsonwebtoken": "^8.0.0", - "jwt-decode": "^2.1.0", "lodash": "^4.16.4" }, "devDependencies": { - "@types/jsonwebtoken": "7.2.6", - "@types/jwt-decode": "2.2.1", "rimraf": "2.6.2", "@accounts/token-manager": "^0.1.0-beta.10" } diff --git a/packages/server/src/accounts-server.ts b/packages/server/src/accounts-server.ts index ad42f18b5..3f6c91ad9 100644 --- a/packages/server/src/accounts-server.ts +++ b/packages/server/src/accounts-server.ts @@ -1,7 +1,6 @@ import * as pick from 'lodash/pick'; import * as omit from 'lodash/omit'; import * as isString from 'lodash/isString'; -import * as jwt from 'jsonwebtoken'; import * as Emittery from 'emittery'; import { AccountsError } from '@accounts/common'; import { @@ -17,8 +16,6 @@ import { TokenRecord, } from '@accounts/types'; -import { generateAccessToken, generateRefreshToken, generateRandomToken } from './utils/tokens'; - import { emailTemplates, sendMail } from './utils/email'; import { ServerHooks } from './utils/server-hooks'; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 06c379719..48344b77a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,14 +1,10 @@ import { AccountsServer } from './accounts-server'; -import * as encryption from './utils/encryption'; -import { generateRandomToken } from './utils/tokens'; import { getFirstUserEmail } from './utils/get-first-user-email'; import { ServerHooks } from './utils/server-hooks'; export default AccountsServer; export { AccountsServer, - encryption, ServerHooks, - generateRandomToken, getFirstUserEmail }; diff --git a/packages/server/src/utils/encryption.ts b/packages/server/src/utils/encryption.ts deleted file mode 100644 index 6353eb94e..000000000 --- a/packages/server/src/utils/encryption.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as bcrypt from 'bcryptjs'; -import { createHash } from 'crypto'; -import * as isString from 'lodash/isString'; - -const bcryptPassword = async password => { - const salt = await bcrypt.genSalt(10); - const hash = await bcrypt.hash(password, salt); - return hash; -}; - -const hashPassword = (password, algorithm) => { - if (isString(password)) { - const hash = createHash(algorithm); - hash.update(password); - return hash.digest('hex'); - } - - return password.digest; -}; - -const verifyPassword = async (password, hash) => bcrypt.compare(password, hash); - -export { bcryptPassword, hashPassword, verifyPassword }; diff --git a/packages/server/src/utils/tokens.ts b/packages/server/src/utils/tokens.ts deleted file mode 100644 index 3754536c7..000000000 --- a/packages/server/src/utils/tokens.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as jwt from 'jsonwebtoken'; -import { randomBytes } from 'crypto'; - -/** - * Generate a random token string - */ -export const generateRandomToken = (length: number = 43): string => - randomBytes(length).toString('hex'); - -export const generateAccessToken = ({ - secret, - data, - config, -}: { - secret: string; - data?: any; - config: object; -}) => - jwt.sign( - { - data, - }, - secret, - config - ); - -export const generateRefreshToken = ({ - secret, - data, - config, -}: { - secret: string; - data?: any; - config: object; -}) => - jwt.sign( - { - data, - }, - secret, - config - ); From 2fc023961a7b1b0719e4b48aea6f4d4d6f1485a0 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 12:34:41 +0200 Subject: [PATCH 7/9] Removed default config and cleaned AccountsServerOptions Interface --- packages/server/src/accounts-server.ts | 14 -------------- .../server/src/types/accounts-server-options.ts | 10 ---------- 2 files changed, 24 deletions(-) diff --git a/packages/server/src/accounts-server.ts b/packages/server/src/accounts-server.ts index 3f6c91ad9..de421e7bb 100644 --- a/packages/server/src/accounts-server.ts +++ b/packages/server/src/accounts-server.ts @@ -24,15 +24,6 @@ import { JwtData } from './types/jwt-data'; import { EmailTemplateType } from './types/email-template-type'; const defaultOptions = { - tokenSecret: 'secret', - tokenConfigs: { - accessToken: { - expiresIn: '90m', - }, - refreshToken: { - expiresIn: '7d', - }, - }, emailTemplates, userObjectSanitizer: (user: User) => user, sendMail, @@ -312,7 +303,6 @@ export class AccountsServer { * @returns {Promise} - Return a new accessToken and refreshToken. */ public createTokens(token: string, isImpersonated: boolean = false): Tokens { - const { tokenSecret, tokenConfigs } = this.options; const jwtData: JwtData = { token, isImpersonated }; const accessToken = this.tokenManager.generateAccessToken(jwtData); const refreshToken = this.tokenManager.generateRefreshToken(); @@ -454,10 +444,6 @@ export class AccountsServer { return this.db.setProfile(userId, { ...user.profile, ...profile }); } - public isTokenExpired(token: string, tokenRecord?: TokenRecord): boolean { - return !tokenRecord || Number(tokenRecord.when) + this.options.emailTokensExpiry < Date.now(); - } - public prepareMail( to: string, token: string, diff --git a/packages/server/src/types/accounts-server-options.ts b/packages/server/src/types/accounts-server-options.ts index eaa3895f0..9b7de4861 100644 --- a/packages/server/src/types/accounts-server-options.ts +++ b/packages/server/src/types/accounts-server-options.ts @@ -9,16 +9,6 @@ import { SendMailType } from './send-mail-type'; export interface AccountsServerOptions { db: DatabaseInterface; tokenManager: any; - tokenSecret: string; - tokenConfigs?: { - accessToken?: { - expiresIn?: string; - }; - refreshToken?: { - expiresIn?: string; - }; - }; - emailTokensExpiry?: number; emailTemplates?: EmailTemplatesType; userObjectSanitizer?: UserObjectSanitizerFunction; impersonationAuthorize?: (user: User, impersonateToUser: User) => Promise; From 357da373a1472941455c356a719877ce90624683 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 13:48:11 +0200 Subject: [PATCH 8/9] Use accounts/error package --- packages/token-manager/package.json | 3 ++- packages/token-manager/src/token-manager.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/token-manager/package.json b/packages/token-manager/package.json index 924267ccb..95f7c0b30 100644 --- a/packages/token-manager/package.json +++ b/packages/token-manager/package.json @@ -27,7 +27,8 @@ "bcryptjs": "^2.4.0", "jsonwebtoken": "^8.0.0", "jwt-decode": "^2.1.0", - "@accounts/types": "^0.1.0-beta.10" + "@accounts/types": "^0.1.0-beta.10", + "@accounts/error": "^0.1.0-beta.10" }, "devDependencies": { "@types/jsonwebtoken": "7.2.5", diff --git a/packages/token-manager/src/token-manager.ts b/packages/token-manager/src/token-manager.ts index 091ef995f..b462edf35 100644 --- a/packages/token-manager/src/token-manager.ts +++ b/packages/token-manager/src/token-manager.ts @@ -1,4 +1,5 @@ import { TokenRecord } from '@accounts/types'; +import AccountsError from '@accounts/error'; import { randomBytes } from 'crypto'; import * as jwt from 'jsonwebtoken'; @@ -58,10 +59,10 @@ export default class TokenManager { private validateConfiguration(config: Configuration): void { if (!config) { - throw new Error('[ Accounts - TokenManager ] configuration : A configuration object is needed'); + throw new AccountsError('TokenManager', 'configuration', 'A configuration object is needed'); } if (typeof config.secret !== 'string') { - throw new Error('[ Accounts - TokenManager ] configuration : A string secret property is needed'); + throw new AccountsError('TokenManager', 'configuration', 'A string secret property is needed'); } } } From 75421444d012dd0639ed3f25d872157149c225c0 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 3 Apr 2018 13:53:35 +0200 Subject: [PATCH 9/9] Added real typings --- packages/server/package.json | 4 ++-- packages/server/src/accounts-server.ts | 3 ++- packages/server/src/types/accounts-server-options.ts | 3 ++- packages/token-manager/src/token-manager.ts | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index e0d9fe5f7..da6fada93 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -48,12 +48,12 @@ "dependencies": { "@accounts/common": "^0.1.0-beta.10", "@accounts/types": "^0.1.0-beta.10", + "@accounts/token-manager": "^0.1.0-beta.10", "babel-polyfill": "^6.23.0", "emittery": "^0.3.0", "lodash": "^4.16.4" }, "devDependencies": { - "rimraf": "2.6.2", - "@accounts/token-manager": "^0.1.0-beta.10" + "rimraf": "2.6.2" } } diff --git a/packages/server/src/accounts-server.ts b/packages/server/src/accounts-server.ts index de421e7bb..fd555639e 100644 --- a/packages/server/src/accounts-server.ts +++ b/packages/server/src/accounts-server.ts @@ -3,6 +3,7 @@ import * as omit from 'lodash/omit'; import * as isString from 'lodash/isString'; import * as Emittery from 'emittery'; import { AccountsError } from '@accounts/common'; +import TokenManager from '@accounts/token-manager'; import { User, LoginResult, @@ -32,7 +33,7 @@ const defaultOptions = { export class AccountsServer { public options: AccountsServerOptions; - public tokenManager: any; + public tokenManager: TokenManager; private services: { [key: string]: AuthenticationService }; private db: DatabaseInterface; private hooks: Emittery; diff --git a/packages/server/src/types/accounts-server-options.ts b/packages/server/src/types/accounts-server-options.ts index 9b7de4861..cabab3a0f 100644 --- a/packages/server/src/types/accounts-server-options.ts +++ b/packages/server/src/types/accounts-server-options.ts @@ -1,3 +1,4 @@ +import TokenManager from '@accounts/token-manager'; import { User, DatabaseInterface } from '@accounts/types'; import { EmailTemplateType } from './email-template-type'; import { EmailTemplatesType } from './email-templates-type'; @@ -8,7 +9,7 @@ import { SendMailType } from './send-mail-type'; export interface AccountsServerOptions { db: DatabaseInterface; - tokenManager: any; + tokenManager: TokenManager; emailTemplates?: EmailTemplatesType; userObjectSanitizer?: UserObjectSanitizerFunction; impersonationAuthorize?: (user: User, impersonateToUser: User) => Promise; diff --git a/packages/token-manager/src/token-manager.ts b/packages/token-manager/src/token-manager.ts index b462edf35..712512dc2 100644 --- a/packages/token-manager/src/token-manager.ts +++ b/packages/token-manager/src/token-manager.ts @@ -37,7 +37,7 @@ export default class TokenManager { this.refreshTokenConfig = { ...defaultTokenConfig, ...defaultRefreshTokenConfig, ...config.refresh }; } - public generateRandomToken(length: number | undefined): string { + public generateRandomToken(length?: number | undefined): string { return randomBytes(length || 43).toString('hex'); } @@ -53,7 +53,7 @@ export default class TokenManager { return !tokenRecord || Number(tokenRecord.when) + this.emailTokenExpiration < Date.now(); } - public decodeToken(token: string, ignoreExpiration: boolean = false): string | object { + public decodeToken(token: string, ignoreExpiration: boolean = false): any { return jwt.verify(token, this.secret, { ignoreExpiration }); }