From 980211d537edb6b51e287b114f41fdfb04bbd013 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 13 Mar 2018 15:23:56 +0100 Subject: [PATCH 1/5] @accounts/token-manager : create empty package --- packages/token-manager/package.json | 37 ++++++++++++++++++++++++++++ packages/token-manager/tsconfig.json | 12 +++++++++ 2 files changed, 49 insertions(+) create mode 100644 packages/token-manager/package.json create mode 100644 packages/token-manager/tsconfig.json diff --git a/packages/token-manager/package.json b/packages/token-manager/package.json new file mode 100644 index 000000000..43bfc0315 --- /dev/null +++ b/packages/token-manager/package.json @@ -0,0 +1,37 @@ +{ + "name": "@accounts/token-manager", + "version": "0.1.0-beta.3", + "license": "MIT", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "start": "tsc --watch", + "compile": "tsc", + "testonly": "jest", + "test:watch": "jest --watch", + "coverage": "jest --coverage", + "prepublishOnly": "yarn compile" + }, + "jest": { + "transform": { + ".(ts|tsx)": "/../../node_modules/ts-jest/preprocessor.js" + }, + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", + "moduleFileExtensions": [ + "ts", + "js" + ], + "mapCoverage": true + }, + "dependencies": { + "bcryptjs": "^2.4.0", + "jsonwebtoken": "^8.0.0", + "jwt-decode": "^2.1.0" + }, + "devDependencies": { + "@accounts/common": "^0.1.0-beta.3", + "@types/jsonwebtoken": "7.2.5", + "@types/jwt-decode": "2.2.1", + "rimraf": "2.6.2" + } +} diff --git a/packages/token-manager/tsconfig.json b/packages/token-manager/tsconfig.json new file mode 100644 index 000000000..5d25a5a05 --- /dev/null +++ b/packages/token-manager/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + }, + "exclude": [ + "node_modules", + "__tests__", + "lib" + ] +} From 73e2e1d25103059c44cdbca68b5437eecfe0701f Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 13 Mar 2018 15:27:08 +0100 Subject: [PATCH 2/5] @accounts/token-manager : added default configuration and configuration types --- packages/token-manager/src/index.ts | 1 + packages/token-manager/src/token-manager.ts | 26 +++++++++++++++++++ .../token-manager/src/types/configuration.ts | 13 ++++++++++ .../types/token-generation-configuration.ts | 25 ++++++++++++++++++ 4 files changed, 65 insertions(+) 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 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..10945dec0 --- /dev/null +++ b/packages/token-manager/src/token-manager.ts @@ -0,0 +1,26 @@ +import { randomBytes } from 'crypto'; +import * as jwt from 'jsonwebtoken'; +import { error } from 'util'; + +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; + +} 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,*/ +} From 6735d658e46ffc12dec7a22f8dc00855e3a56bc8 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 13 Mar 2018 15:28:20 +0100 Subject: [PATCH 3/5] @accounts/token-manager : class initialisation and configuration validation --- packages/token-manager/src/token-manager.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/token-manager/src/token-manager.ts b/packages/token-manager/src/token-manager.ts index 10945dec0..9496dea8c 100644 --- a/packages/token-manager/src/token-manager.ts +++ b/packages/token-manager/src/token-manager.ts @@ -18,9 +18,29 @@ const defaultRefreshTokenConfig: TokenGenerationConfiguration = { }; 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 }; + } + + 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'); + } + } } From 910ebdfd4ee16ac62c88924983ca1b7bf87fb351 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 13 Mar 2018 15:29:28 +0100 Subject: [PATCH 4/5] @accounts/token-manager : added class methods --- packages/token-manager/src/token-manager.ts | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/token-manager/src/token-manager.ts b/packages/token-manager/src/token-manager.ts index 9496dea8c..55fa2d102 100644 --- a/packages/token-manager/src/token-manager.ts +++ b/packages/token-manager/src/token-manager.ts @@ -1,3 +1,5 @@ +import { TokenRecord } from '@accounts/common'; + import { randomBytes } from 'crypto'; import * as jwt from 'jsonwebtoken'; import { error } from 'util'; @@ -35,6 +37,26 @@ export default class TokenManager { 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'); From 435a24f5e795e74010d54561c04641d4ff7d2ea2 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Tue, 13 Mar 2018 15:30:37 +0100 Subject: [PATCH 5/5] @accounts/token-manager : added tests --- packages/token-manager/__tests__/index.ts | 7 ++ .../token-manager/__tests__/token-manager.ts | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 packages/token-manager/__tests__/index.ts create mode 100644 packages/token-manager/__tests__/token-manager.ts 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..f96f30d54 --- /dev/null +++ b/packages/token-manager/__tests__/token-manager.ts @@ -0,0 +1,84 @@ +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 throw when no parameters provided', () => { + expect(() => { + TM.generateAccessToken(); + }).toThrow(); + }); + + 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).user).toBe('test'); + }); + }); +});