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'); + }); + }); +}); 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/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..55fa2d102 --- /dev/null +++ b/packages/token-manager/src/token-manager.ts @@ -0,0 +1,68 @@ +import { TokenRecord } from '@accounts/common'; + +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; + + 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..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" + ] +}