From d96d2e9e671d5e8925810b48385db6d20c05c759 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Fri, 25 Jan 2019 15:40:07 -0500 Subject: [PATCH] feat: create jwt auth service Signed-off-by: jannyHou --- package-lock.json | 44 +++----- src/application.ts | 18 +++ src/authentication-strategies/JWT.strategy.ts | 16 ++- src/controllers/user.controller.ts | 19 ++-- src/keys.ts | 22 ++++ src/providers/strategy.resolver.provider.ts | 5 +- src/services/JWT.authentication.service.ts | 106 ++++++++++++++++++ src/services/hash.password.bcryptjs.ts | 19 ++++ src/utils/user.authentication.ts | 81 ------------- test/acceptance/user.controller.acceptance.ts | 13 ++- test/unit/utils.authentication.unit.ts | 28 +++-- 11 files changed, 231 insertions(+), 140 deletions(-) create mode 100644 src/keys.ts create mode 100644 src/services/JWT.authentication.service.ts create mode 100644 src/services/hash.password.bcryptjs.ts delete mode 100644 src/utils/user.authentication.ts diff --git a/package-lock.json b/package-lock.json index fbfcc4b92..4c8c44af8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "integrity": "sha512-WXKf5K5HT6X0kKiCOezJZFljsfxKV1FpU8Tf1A7ZpGvyd/Q4hlrJm2EwoH2onaUq3O4tLDp+4gk0hHPsMyxmOg==", "dev": true }, +<<<<<<< HEAD "@babel/runtime": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", @@ -89,6 +90,8 @@ } } }, +======= +>>>>>>> b7c5bd0... feat: create jwt auth service "@babel/template": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", @@ -630,7 +633,7 @@ "@types/supertest": "2.0.7", "express": "4.16.4", "fs-extra": "7.0.1", - "oas-validator": "2.0.2", + "oas-validator": "2.0.1", "shot": "4.0.7", "should": "13.2.3", "sinon": "7.2.2", @@ -687,7 +690,6 @@ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "dev": true, "requires": { - "co": "4.6.0", "fast-deep-equal": "1.1.0", "fast-json-stable-stringify": "2.0.0", "json-schema-traverse": "0.3.1" @@ -806,7 +808,6 @@ "dev": true, "requires": { "js-yaml": "3.12.0", - "node-fetch-h2": "2.3.0", "oas-kit-common": "1.0.6", "reftools": "1.0.4", "yargs": "12.0.5" @@ -825,7 +826,6 @@ "dev": true, "requires": { "ajv": "5.5.2", - "better-ajv-errors": "0.5.7", "js-yaml": "3.12.0", "oas-kit-common": "1.0.6", "oas-linter": "2.0.1", @@ -1421,6 +1421,7 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, +<<<<<<< HEAD "better-ajv-errors": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.5.7.tgz", @@ -1449,6 +1450,8 @@ } } }, +======= +>>>>>>> b7c5bd0... feat: create jwt auth service "bl": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.0.1.tgz", @@ -1692,6 +1695,7 @@ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz", "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg==" }, +<<<<<<< HEAD "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1704,6 +1708,8 @@ "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==", "dev": true }, +======= +>>>>>>> b7c5bd0... feat: create jwt auth service "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2960,12 +2966,6 @@ "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.3.1.tgz", "integrity": "sha512-PcI9NUm6EUOhHlaxYABCqDQQWS7IgoBZ/PmPkhuzj+oR01ffjv3EJfKnnWJZcUhILtUh6/NdJi1Zs/mIr6v8DA==" }, - "http2-client": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.1.tgz", - "integrity": "sha512-3oWAo1MOb8VlQ26wxydOP7cD4Kt0KAtMZhTrl2HWjBtiLCQ80zgr1mvt6iNyKiVTN2wHwOP290b8KfErNcVZlg==", - "dev": true - }, "husky": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/husky/-/husky-1.1.3.tgz", @@ -3570,6 +3570,7 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, +<<<<<<< HEAD "json-to-ast": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz", @@ -3580,6 +3581,8 @@ "grapheme-splitter": "1.0.4" } }, +======= +>>>>>>> b7c5bd0... feat: create jwt auth service "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3600,12 +3603,6 @@ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.18.0.tgz", "integrity": "sha512-YCLSH4SkHbNCNfJ/IeBGmKwPaiFxTZU011F5MV0UcC+O+te7fiwOQMU2ZYFCkqkH1VkhFRKxftoODbRd7YafeA==" }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, "jsonwebtoken": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz", @@ -3669,12 +3666,6 @@ "invert-kv": "2.0.0" } }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, "linkify-it": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", @@ -4415,15 +4406,6 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dev": true, - "requires": { - "http2-client": "1.3.1" - } - }, "nopt": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz", diff --git a/src/application.ts b/src/application.ts index f683d4d85..091a15b6c 100644 --- a/src/application.ts +++ b/src/application.ts @@ -14,8 +14,15 @@ import { AuthenticationBindings, AuthenticationComponent, } from '@loopback/authentication'; +import {JWTAuthenticationBindings, OtherServicesBindings} from './keys'; import {StrategyResolverProvider} from './providers/strategy.resolver.provider'; import {AuthenticateActionProvider} from './providers/custom.authentication.provider'; +import { + JWTAuthenticationService, + JWT_SECRET, +} from './services/JWT.authentication.service'; +import {hashPassword} from './services/hash.password.bcryptjs'; +import {JWTStrategy} from './authentication-strategies/JWT.strategy'; /** * Information from package.json @@ -38,6 +45,7 @@ export class ShoppingApplication extends BootMixin( // Bind package.json to the application context this.bind(PackageKey).to(pkg); + // Bind authentication component related elements this.component(AuthenticationComponent); this.bind(AuthenticationBindings.AUTH_ACTION).toProvider( AuthenticateActionProvider, @@ -46,6 +54,16 @@ export class ShoppingApplication extends BootMixin( StrategyResolverProvider, ); + // Bind JWT authentication strategy related elements + this.bind(JWTAuthenticationBindings.STRATEGY).toClass(JWTStrategy); + this.bind(JWTAuthenticationBindings.SECRET).to(JWT_SECRET); + this.bind(JWTAuthenticationBindings.SERVICE).toClass( + JWTAuthenticationService, + ); + + // Bind other services + this.bind(OtherServicesBindings.HASH_PASSWORD).to(hashPassword); + // Set up the custom sequence this.sequence(MySequence); diff --git a/src/authentication-strategies/JWT.strategy.ts b/src/authentication-strategies/JWT.strategy.ts index ea093859d..f84c71500 100644 --- a/src/authentication-strategies/JWT.strategy.ts +++ b/src/authentication-strategies/JWT.strategy.ts @@ -3,14 +3,20 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -// Consider turn it to a binding -const SECRET = 'secretforjwt'; +import {JWTAuthenticationBindings} from '../keys'; import {Request, HttpErrors} from '@loopback/rest'; import {UserProfile} from '@loopback/authentication'; import {AuthenticationStrategy} from './authentication.strategy'; -import {decodeAccessToken} from '../utils/user.authentication'; +import {inject} from '@loopback/core'; +import {JWTAuthenticationService} from '../services/JWT.authentication.service'; export class JWTStrategy implements AuthenticationStrategy { + constructor( + @inject(JWTAuthenticationBindings.SERVICE) + public jwt_authentication_service: JWTAuthenticationService, + @inject(JWTAuthenticationBindings.SECRET) + public jwt_secret: string, + ) {} async authenticate(request: Request): Promise { let token = request.query.access_token || request.headers['authorization']; if (!token) throw new HttpErrors.Unauthorized('No access token found!'); @@ -20,7 +26,9 @@ export class JWTStrategy implements AuthenticationStrategy { } try { - const user = await decodeAccessToken(token, SECRET); + const user = await this.jwt_authentication_service.decodeAccessToken( + token, + ); return user; } catch (err) { Object.assign(err, { diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 5c7a32bac..1a169b99c 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -15,11 +15,10 @@ import { AuthenticationBindings, } from '@loopback/authentication'; import {Credentials} from '../repositories/user.repository'; -import { - validateCredentials, - getAccessTokenForUser, - hashPassword, -} from '../utils/user.authentication'; +import {HashPassword} from '../services/hash.password.bcryptjs'; +import {JWTAuthenticationService} from '../services/JWT.authentication.service'; +import {JWTAuthenticationBindings, OtherServicesBindings} from '../keys'; +import {validateCredentials} from '../services/JWT.authentication.service'; import * as _ from 'lodash'; // TODO(jannyHou): This should be moved to @loopback/authentication @@ -40,12 +39,16 @@ export class UserController { public recommender: RecommenderService, @inject.setter(AuthenticationBindings.CURRENT_USER) public setCurrentUser: Setter, + @inject(OtherServicesBindings.HASH_PASSWORD) + public hashPassword: HashPassword, + @inject(JWTAuthenticationBindings.SERVICE) + public jwt_authentication_service: JWTAuthenticationService, ) {} @post('/users') async create(@requestBody() user: User): Promise { validateCredentials(_.pick(user, ['email', 'password'])); - user.password = await hashPassword(user.password, 10); + user.password = await this.hashPassword(user.password, 10); // Save & Return Result const savedUser = await this.userRepository.create(user); @@ -143,7 +146,9 @@ export class UserController { @requestBody() credentials: Credentials, ): Promise<{token: string}> { validateCredentials(credentials); - const token = await getAccessTokenForUser(this.userRepository, credentials); + const token = await this.jwt_authentication_service.getAccessTokenForUser( + credentials, + ); return {token}; } } diff --git a/src/keys.ts b/src/keys.ts new file mode 100644 index 000000000..81eaf3fd8 --- /dev/null +++ b/src/keys.ts @@ -0,0 +1,22 @@ +import {BindingKey} from '@loopback/context'; +import {JWTAuthenticationService} from './services/JWT.authentication.service'; +import {HashPassword} from './services/hash.password.bcryptjs'; +import {JWTStrategy} from './authentication-strategies/JWT.strategy'; + +// Discussion point for reviewers: +// What would be the good naming conversion for bindings? +export namespace JWTAuthenticationBindings { + export const STRATEGY = BindingKey.create( + 'authentication.strategies.jwt.strategy', + ); + export const SECRET = BindingKey.create('authentication.jwt.secret'); + export const SERVICE = BindingKey.create( + 'services.authentication.jwt.service', + ); +} + +export namespace OtherServicesBindings { + export const HASH_PASSWORD = BindingKey.create( + 'services.hash_password', + ); +} diff --git a/src/providers/strategy.resolver.provider.ts b/src/providers/strategy.resolver.provider.ts index 4de77258e..35c60d9e3 100644 --- a/src/providers/strategy.resolver.provider.ts +++ b/src/providers/strategy.resolver.provider.ts @@ -10,11 +10,14 @@ import { AuthenticationMetadata, } from '@loopback/authentication'; import {JWTStrategy} from '../authentication-strategies/JWT.strategy'; +import {JWTAuthenticationBindings} from '../keys'; export class StrategyResolverProvider implements Provider { constructor( @inject(AuthenticationBindings.METADATA) private metadata: AuthenticationMetadata, + @inject(JWTAuthenticationBindings.STRATEGY) + private jwt_strategy: JWTStrategy, ) {} value(): ValueOrPromise { if (!this.metadata) { @@ -24,7 +27,7 @@ export class StrategyResolverProvider const name = this.metadata.strategy; // This should be extensible if (name === 'jwt') { - return new JWTStrategy(); + return this.jwt_strategy; } else { throw new Error(`The strategy ${name} is not available.`); } diff --git a/src/services/JWT.authentication.service.ts b/src/services/JWT.authentication.service.ts new file mode 100644 index 000000000..18219b4a2 --- /dev/null +++ b/src/services/JWT.authentication.service.ts @@ -0,0 +1,106 @@ +// Copyright IBM Corp. 2018, 2019. All Rights Reserved. +// Node module: @loopback4-example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as _ from 'lodash'; +import {Credentials, UserRepository} from '../repositories/user.repository'; +import {toJSON} from '@loopback/testlab'; +import {promisify} from 'util'; +import * as isemail from 'isemail'; +import {HttpErrors} from '@loopback/rest'; +import {UserProfile} from '@loopback/authentication'; +import {compare} from 'bcryptjs'; +import {repository} from '@loopback/repository'; +import {inject} from '@loopback/core'; +import {JWTAuthenticationBindings} from '../keys'; +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +const verifyAsync = promisify(jwt.verify); + +/** + * Constant for JWT secret string + */ +export const JWT_SECRET = 'jwtsecret'; + +/** + * A JWT authentication service that could be reused by + * different clients. Usually it can be injected in the + * controller constructor. + * It provides services that handle the logics between the controller layer + * and the repository layer. + */ +export class JWTAuthenticationService { + constructor( + @repository(UserRepository) public userRepository: UserRepository, + @inject(JWTAuthenticationBindings.SECRET) public jwt_secret: string, + ) {} + + /** + * A function that retrieves the user with given credentials. Generates + * JWT access token using user profile as payload if user found. + * + * Usually a request's corresponding controller function filters the credential + * fields and invokes this function. + * + * @param credentials The user credentials including email and password. + */ + async getAccessTokenForUser(credentials: Credentials): Promise { + const foundUser = await this.userRepository.findOne({ + where: {email: credentials.email}, + }); + if (!foundUser) { + throw new HttpErrors['NotFound']( + `User with email ${credentials.email} not found.`, + ); + } + const passwordMatched = await compare( + credentials.password, + foundUser.password, + ); + if (!passwordMatched) { + throw new HttpErrors.Unauthorized('The credentials are not correct.'); + } + + const currentUser = _.pick(toJSON(foundUser), ['id', 'email', 'firstName']); + // Generate user token using JWT + const token = await signAsync(currentUser, this.jwt_secret, { + expiresIn: 300, + }); + + return token; + } + + /** + * Decodes the user's information from a valid JWT access token. + * Then generate a `UserProfile` instance as the returned user. + * + * @param token A JWT access token. + */ + async decodeAccessToken(token: string): Promise { + const decoded = await verifyAsync(token, this.jwt_secret); + let user = _.pick(decoded, ['id', 'email', 'firstName']); + (user as UserProfile).name = user.firstName; + delete user.firstName; + return user; + } +} + +/** + * To be removed in story + * https://github.com/strongloop/loopback4-example-shopping/issues/39 + * @param credentials + */ +export function validateCredentials(credentials: Credentials) { + // Validate Email + if (!isemail.validate(credentials.email)) { + throw new HttpErrors.UnprocessableEntity('invalid email'); + } + + // Validate Password Length + if (credentials.password.length < 8) { + throw new HttpErrors.UnprocessableEntity( + 'password must be minimum 8 characters', + ); + } +} diff --git a/src/services/hash.password.bcryptjs.ts b/src/services/hash.password.bcryptjs.ts new file mode 100644 index 000000000..cac696eb1 --- /dev/null +++ b/src/services/hash.password.bcryptjs.ts @@ -0,0 +1,19 @@ +import {genSalt, hash} from 'bcryptjs'; + +/** + * Service HashPassword using module 'bcryptjs'. + * It takes in a plain password, generates a salt with given + * round and returns the hashed password as a string + */ +export type HashPassword = ( + password: string, + rounds: number, +) => Promise; +// bind function to `services.bcryptjs.HashPassword` +export async function hashPassword( + password: string, + rounds: number, +): Promise { + const salt = await genSalt(rounds); + return await hash(password, salt); +} diff --git a/src/utils/user.authentication.ts b/src/utils/user.authentication.ts deleted file mode 100644 index 373e736b4..000000000 --- a/src/utils/user.authentication.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright IBM Corp. 2018, 2019. All Rights Reserved. -// Node module: @loopback4-example-shopping -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import * as _ from 'lodash'; -import {Credentials, UserRepository} from '../repositories/user.repository'; -import {toJSON} from '@loopback/testlab'; -import {promisify} from 'util'; -import * as isemail from 'isemail'; -import {HttpErrors} from '@loopback/rest'; -import {UserProfile} from '@loopback/authentication'; -import {genSalt, compare, hash} from 'bcryptjs'; -const jwt = require('jsonwebtoken'); -const signAsync = promisify(jwt.sign); -const verifyAsync = promisify(jwt.verify); - -export async function hashPassword( - password: string, - rounds: number, -): Promise { - const salt = await genSalt(rounds); - return await hash(password, salt); -} - -export async function getAccessTokenForUser( - userRepository: UserRepository, - credentials: Credentials, -): Promise { - const foundUser = await userRepository.findOne({ - where: {email: credentials.email}, - }); - if (!foundUser) { - throw new HttpErrors['NotFound']( - `User with email ${credentials.email} not found.`, - ); - } - const passwordMatched = await compare( - credentials.password, - foundUser.password, - ); - if (!passwordMatched) { - throw new HttpErrors.Unauthorized('The credentials are not correct.'); - } - - const currentUser = _.pick(toJSON(foundUser), ['id', 'email', 'firstName']); - // Generate user token using JWT - const token = await signAsync(currentUser, 'secretforjwt', { - expiresIn: 300, - }); - - return token; -} - -export function validateCredentials(credentials: Credentials) { - // Validate Email - if (!isemail.validate(credentials.email)) { - throw new HttpErrors.UnprocessableEntity('invalid email'); - } - - // Validate Password Length - if (credentials.password.length < 8) { - throw new HttpErrors.UnprocessableEntity( - 'password must be minimum 8 characters', - ); - } -} - -// secret should be injected -// the refactor PR is in progress -// https://github.com/strongloop/loopback4-example-shopping/pull/33 -export async function decodeAccessToken( - token: string, - secret: string, -): Promise { - const decoded = await verifyAsync(token, secret); - let user = _.pick(decoded, ['id', 'email', 'firstName']); - (user as UserProfile).name = user.firstName; - delete user.firstName; - return user; -} diff --git a/test/acceptance/user.controller.acceptance.ts b/test/acceptance/user.controller.acceptance.ts index 90406bbfc..698d719d8 100644 --- a/test/acceptance/user.controller.acceptance.ts +++ b/test/acceptance/user.controller.acceptance.ts @@ -13,9 +13,10 @@ import {createRecommendationServer} from '../../recommender'; import {Server} from 'http'; import * as _ from 'lodash'; import { - getAccessTokenForUser, - hashPassword, -} from '../../src/utils/user.authentication'; + JWT_SECRET, + JWTAuthenticationService, +} from '../../src/services/JWT.authentication.service'; +import {hashPassword} from '../../src/services/hash.password.bcryptjs'; const recommendations = require('../../recommender/recommendations.json'); describe('UserController', () => { @@ -139,10 +140,12 @@ describe('UserController', () => { describe('authentication functions', () => { let plainPassword: string; + let jwt_auth_service: JWTAuthenticationService; before('create new user', async () => { plainPassword = user.password; user.password = await hashPassword(user.password, 4); + jwt_auth_service = new JWTAuthenticationService(userRepo, JWT_SECRET); }); it('login returns a valid token', async () => { @@ -170,7 +173,7 @@ describe('UserController', () => { it('/me returns the current user', async () => { const newUser = await userRepo.create(user); - const token = await getAccessTokenForUser(userRepo, { + const token = await jwt_auth_service.getAccessTokenForUser({ email: newUser.email, password: plainPassword, }); @@ -186,7 +189,7 @@ describe('UserController', () => { it('/me returns 401 when the token is not provided', async () => { const newUser = await userRepo.create(user); - await getAccessTokenForUser(userRepo, { + await jwt_auth_service.getAccessTokenForUser({ email: newUser.email, password: plainPassword, }); diff --git a/test/unit/utils.authentication.unit.ts b/test/unit/utils.authentication.unit.ts index a27048d1f..c3725f851 100644 --- a/test/unit/utils.authentication.unit.ts +++ b/test/unit/utils.authentication.unit.ts @@ -6,16 +6,15 @@ import {expect, toJSON} from '@loopback/testlab'; import {MongoDataSource} from '../../src/datasources'; import { - decodeAccessToken, - getAccessTokenForUser, - hashPassword, -} from '../../src/utils/user.authentication'; + JWT_SECRET, + JWTAuthenticationService, +} from '../../src/services/JWT.authentication.service'; +import {hashPassword} from '../../src/services/hash.password.bcryptjs'; import {UserRepository, OrderRepository} from '../../src/repositories'; import {User} from '../../src/models'; import * as _ from 'lodash'; import {JsonWebTokenError} from 'jsonwebtoken'; import {HttpErrors} from '@loopback/rest'; -const SECRET = 'secretforjwt'; describe('authentication utilities', () => { const mongodbDS = new MongoDataSource(); @@ -28,12 +27,14 @@ describe('authentication utilities', () => { surname: 'test', }; let newUser: User; + let jwt_service: JWTAuthenticationService; before(clearDatabase); before(createUser); + before(createService); it('getAccessTokenForUser creates valid jwt access token', async () => { - const token = await getAccessTokenForUser(userRepo, { + const token = await jwt_service.getAccessTokenForUser({ email: 'unittest@loopback.io', password: 'p4ssw0rd', }); @@ -45,7 +46,7 @@ describe('authentication utilities', () => { `User with email fake@loopback.io not found.`, ); return expect( - getAccessTokenForUser(userRepo, { + jwt_service.getAccessTokenForUser({ email: 'fake@loopback.io', password: 'fake', }), @@ -57,7 +58,7 @@ describe('authentication utilities', () => { 'The credentials are not correct.', ); return expect( - getAccessTokenForUser(userRepo, { + jwt_service.getAccessTokenForUser({ email: 'unittest@loopback.io', password: 'fake', }), @@ -65,19 +66,21 @@ describe('authentication utilities', () => { }); it('decodeAccessToken decodes valid access token', async () => { - const token = await getAccessTokenForUser(userRepo, { + const token = await jwt_service.getAccessTokenForUser({ email: 'unittest@loopback.io', password: 'p4ssw0rd', }); const expectedUser = getExpectedUser(newUser); - const currentUser = await decodeAccessToken(token, SECRET); + const currentUser = await jwt_service.decodeAccessToken(token); expect(currentUser).to.deepEqual(expectedUser); }); it('decodeAccessToken throws error for invalid accesstoken', async () => { const token = 'fake'; const error = new JsonWebTokenError('jwt malformed'); - return expect(decodeAccessToken(token, SECRET)).to.be.rejectedWith(error); + return expect(jwt_service.decodeAccessToken(token)).to.be.rejectedWith( + error, + ); }); async function createUser() { @@ -87,6 +90,9 @@ describe('authentication utilities', () => { async function clearDatabase() { await userRepo.deleteAll(); } + async function createService() { + jwt_service = new JWTAuthenticationService(userRepo, JWT_SECRET); + } }); function getExpectedUser(originalUser: User) {