From 2860083de1a9b78e2db53e196f69b8d9200baeed Mon Sep 17 00:00:00 2001 From: jannyHou Date: Thu, 14 Feb 2019 22:54:19 -0500 Subject: [PATCH] refactor: bcrypt hash Signed-off-by: jannyHou --- src/application.ts | 9 ++-- src/controllers/user.controller.ts | 16 +++---- src/keys.ts | 9 ++-- src/services/JWT.authentication.service.ts | 10 ++-- src/services/hash.password.bcryptjs.ts | 28 +++++++++++ test/acceptance/user.controller.acceptance.ts | 23 ++++++---- test/unit/helper.ts | 18 ++++++++ test/unit/utils.authentication.unit.ts | 46 +++++++++++++------ 8 files changed, 116 insertions(+), 43 deletions(-) create mode 100644 test/unit/helper.ts diff --git a/src/application.ts b/src/application.ts index 091a15b6c..e1d8cc85e 100644 --- a/src/application.ts +++ b/src/application.ts @@ -14,14 +14,14 @@ import { AuthenticationBindings, AuthenticationComponent, } from '@loopback/authentication'; -import {JWTAuthenticationBindings, OtherServicesBindings} from './keys'; +import {JWTAuthenticationBindings, PasswordHasherBindings} 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 {BcryptHasher} from './services/hash.password.bcryptjs'; import {JWTStrategy} from './authentication-strategies/JWT.strategy'; /** @@ -61,8 +61,9 @@ export class ShoppingApplication extends BootMixin( JWTAuthenticationService, ); - // Bind other services - this.bind(OtherServicesBindings.HASH_PASSWORD).to(hashPassword); + // Bind bcrypt hash services + this.bind(PasswordHasherBindings.ROUNDS).to(10); + this.bind(PasswordHasherBindings.PASSWORD_HASHER).toClass(BcryptHasher); // Set up the custom sequence this.sequence(MySequence); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 1a169b99c..cc60377bd 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -15,9 +15,9 @@ import { AuthenticationBindings, } from '@loopback/authentication'; import {Credentials} from '../repositories/user.repository'; -import {HashPassword} from '../services/hash.password.bcryptjs'; +import {PasswordHasher} from '../services/hash.password.bcryptjs'; import {JWTAuthenticationService} from '../services/JWT.authentication.service'; -import {JWTAuthenticationBindings, OtherServicesBindings} from '../keys'; +import {JWTAuthenticationBindings, PasswordHasherBindings} from '../keys'; import {validateCredentials} from '../services/JWT.authentication.service'; import * as _ from 'lodash'; @@ -39,16 +39,16 @@ export class UserController { public recommender: RecommenderService, @inject.setter(AuthenticationBindings.CURRENT_USER) public setCurrentUser: Setter, - @inject(OtherServicesBindings.HASH_PASSWORD) - public hashPassword: HashPassword, + @inject(PasswordHasherBindings.PASSWORD_HASHER) + public passwordHahser: PasswordHasher, @inject(JWTAuthenticationBindings.SERVICE) - public jwt_authentication_service: JWTAuthenticationService, + public jwtAuthenticationService: JWTAuthenticationService, ) {} @post('/users') async create(@requestBody() user: User): Promise { validateCredentials(_.pick(user, ['email', 'password'])); - user.password = await this.hashPassword(user.password, 10); + user.password = await this.passwordHahser.hashPassword(user.password); // Save & Return Result const savedUser = await this.userRepository.create(user); @@ -99,7 +99,6 @@ export class UserController { // as a stateless authentication method, JWT doesn't actually // have a logout operation. See article for details: // https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6 - @get('/users/{userId}/recommend', { responses: { '200': { @@ -142,11 +141,12 @@ export class UserController { }, }, }) + // @authenticate('jwt', {action: 'generateAccessToken'}) async login( @requestBody() credentials: Credentials, ): Promise<{token: string}> { validateCredentials(credentials); - const token = await this.jwt_authentication_service.getAccessTokenForUser( + const token = await this.jwtAuthenticationService.getAccessTokenForUser( credentials, ); return {token}; diff --git a/src/keys.ts b/src/keys.ts index 81eaf3fd8..540941ba6 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -1,7 +1,7 @@ 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'; +import {PasswordHasher} from './services/hash.password.bcryptjs'; // Discussion point for reviewers: // What would be the good naming conversion for bindings? @@ -15,8 +15,9 @@ export namespace JWTAuthenticationBindings { ); } -export namespace OtherServicesBindings { - export const HASH_PASSWORD = BindingKey.create( - 'services.hash_password', +export namespace PasswordHasherBindings { + export const PASSWORD_HASHER = BindingKey.create( + 'services.hasher', ); + export const ROUNDS = BindingKey.create('services.hasher.round'); } diff --git a/src/services/JWT.authentication.service.ts b/src/services/JWT.authentication.service.ts index 18219b4a2..a914ddbf8 100644 --- a/src/services/JWT.authentication.service.ts +++ b/src/services/JWT.authentication.service.ts @@ -10,10 +10,10 @@ 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'; +import {JWTAuthenticationBindings, PasswordHasherBindings} from '../keys'; +import {PasswordHasher} from './hash.password.bcryptjs'; const jwt = require('jsonwebtoken'); const signAsync = promisify(jwt.sign); const verifyAsync = promisify(jwt.verify); @@ -34,6 +34,8 @@ export class JWTAuthenticationService { constructor( @repository(UserRepository) public userRepository: UserRepository, @inject(JWTAuthenticationBindings.SECRET) public jwt_secret: string, + @inject(PasswordHasherBindings.PASSWORD_HASHER) + public passwordHasher: PasswordHasher, ) {} /** @@ -54,10 +56,12 @@ export class JWTAuthenticationService { `User with email ${credentials.email} not found.`, ); } - const passwordMatched = await compare( + + const passwordMatched = await this.passwordHasher.comparePassword( credentials.password, foundUser.password, ); + if (!passwordMatched) { throw new HttpErrors.Unauthorized('The credentials are not correct.'); } diff --git a/src/services/hash.password.bcryptjs.ts b/src/services/hash.password.bcryptjs.ts index cac696eb1..cd6a302da 100644 --- a/src/services/hash.password.bcryptjs.ts +++ b/src/services/hash.password.bcryptjs.ts @@ -1,4 +1,7 @@ import {genSalt, hash} from 'bcryptjs'; +import {compare} from 'bcryptjs'; +import {inject} from '@loopback/core'; +import {PasswordHasherBindings} from '../keys'; /** * Service HashPassword using module 'bcryptjs'. @@ -17,3 +20,28 @@ export async function hashPassword( const salt = await genSalt(rounds); return await hash(password, salt); } + +export interface PasswordHasher { + hashPassword(password: T): Promise; + comparePassword(providedPass: T, storedPass: T): Promise; +} + +export class BcryptHasher implements PasswordHasher { + constructor( + @inject(PasswordHasherBindings.ROUNDS) + private readonly rounds: number, + ) {} + + async hashPassword(password: string): Promise { + const salt = await genSalt(this.rounds); + return await hash(password, salt); + } + + async comparePassword( + providedPass: string, + storedPass: string, + ): Promise { + const passwordIsMatched = await compare(providedPass, storedPass); + return passwordIsMatched; + } +} diff --git a/test/acceptance/user.controller.acceptance.ts b/test/acceptance/user.controller.acceptance.ts index 698d719d8..ed0ebe004 100644 --- a/test/acceptance/user.controller.acceptance.ts +++ b/test/acceptance/user.controller.acceptance.ts @@ -12,11 +12,11 @@ import {setupApplication} from './helper'; import {createRecommendationServer} from '../../recommender'; import {Server} from 'http'; import * as _ from 'lodash'; +import {JWTAuthenticationService} from '../../src/services/JWT.authentication.service'; import { - JWT_SECRET, - JWTAuthenticationService, -} from '../../src/services/JWT.authentication.service'; -import {hashPassword} from '../../src/services/hash.password.bcryptjs'; + PasswordHasherBindings, + JWTAuthenticationBindings, +} from '../../src/keys'; const recommendations = require('../../recommender/recommendations.json'); describe('UserController', () => { @@ -140,12 +140,17 @@ describe('UserController', () => { describe('authentication functions', () => { let plainPassword: string; - let jwt_auth_service: JWTAuthenticationService; + let jwtAuthService: JWTAuthenticationService; before('create new user', async () => { + app.bind(PasswordHasherBindings.ROUNDS).to(4); + + const passwordHasher = await app.get( + PasswordHasherBindings.PASSWORD_HASHER, + ); plainPassword = user.password; - user.password = await hashPassword(user.password, 4); - jwt_auth_service = new JWTAuthenticationService(userRepo, JWT_SECRET); + user.password = await passwordHasher.hashPassword(user.password); + jwtAuthService = await app.get(JWTAuthenticationBindings.SERVICE); }); it('login returns a valid token', async () => { @@ -173,7 +178,7 @@ describe('UserController', () => { it('/me returns the current user', async () => { const newUser = await userRepo.create(user); - const token = await jwt_auth_service.getAccessTokenForUser({ + const token = await jwtAuthService.getAccessTokenForUser({ email: newUser.email, password: plainPassword, }); @@ -189,7 +194,7 @@ describe('UserController', () => { it('/me returns 401 when the token is not provided', async () => { const newUser = await userRepo.create(user); - await jwt_auth_service.getAccessTokenForUser({ + await jwtAuthService.getAccessTokenForUser({ email: newUser.email, password: plainPassword, }); diff --git a/test/unit/helper.ts b/test/unit/helper.ts new file mode 100644 index 000000000..3c6adab0e --- /dev/null +++ b/test/unit/helper.ts @@ -0,0 +1,18 @@ +// 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 {ShoppingApplication} from '../..'; +import {givenHttpServerConfig} from '@loopback/testlab'; + +export async function setupApplication(): Promise { + const app = new ShoppingApplication({ + rest: givenHttpServerConfig(), + }); + + await app.boot(); + await app.start(); + + return app; +} diff --git a/test/unit/utils.authentication.unit.ts b/test/unit/utils.authentication.unit.ts index c3725f851..71cdf6598 100644 --- a/test/unit/utils.authentication.unit.ts +++ b/test/unit/utils.authentication.unit.ts @@ -5,21 +5,27 @@ import {expect, toJSON} from '@loopback/testlab'; import {MongoDataSource} from '../../src/datasources'; -import { - JWT_SECRET, - JWTAuthenticationService, -} from '../../src/services/JWT.authentication.service'; -import {hashPassword} from '../../src/services/hash.password.bcryptjs'; +import {JWTAuthenticationService} from '../../src/services/JWT.authentication.service'; +import {ShoppingApplication} from '../..'; +import {PasswordHasher} 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'; +import { + PasswordHasherBindings, + JWTAuthenticationBindings, +} from '../../src/keys'; +import {setupApplication} from './helper'; + +describe('authentication services', () => { + let app: ShoppingApplication; -describe('authentication utilities', () => { const mongodbDS = new MongoDataSource(); const orderRepo = new OrderRepository(mongodbDS); const userRepo = new UserRepository(mongodbDS, orderRepo); + const user = { email: 'unittest@loopback.io', password: 'p4ssw0rd', @@ -27,14 +33,16 @@ describe('authentication utilities', () => { surname: 'test', }; let newUser: User; - let jwt_service: JWTAuthenticationService; + let jwtService: JWTAuthenticationService; + let bcryptHasher: PasswordHasher; + before(setupApp); before(clearDatabase); before(createUser); before(createService); it('getAccessTokenForUser creates valid jwt access token', async () => { - const token = await jwt_service.getAccessTokenForUser({ + const token = await jwtService.getAccessTokenForUser({ email: 'unittest@loopback.io', password: 'p4ssw0rd', }); @@ -46,7 +54,7 @@ describe('authentication utilities', () => { `User with email fake@loopback.io not found.`, ); return expect( - jwt_service.getAccessTokenForUser({ + jwtService.getAccessTokenForUser({ email: 'fake@loopback.io', password: 'fake', }), @@ -58,7 +66,7 @@ describe('authentication utilities', () => { 'The credentials are not correct.', ); return expect( - jwt_service.getAccessTokenForUser({ + jwtService.getAccessTokenForUser({ email: 'unittest@loopback.io', password: 'fake', }), @@ -66,32 +74,40 @@ describe('authentication utilities', () => { }); it('decodeAccessToken decodes valid access token', async () => { - const token = await jwt_service.getAccessTokenForUser({ + const token = await jwtService.getAccessTokenForUser({ email: 'unittest@loopback.io', password: 'p4ssw0rd', }); const expectedUser = getExpectedUser(newUser); - const currentUser = await jwt_service.decodeAccessToken(token); + const currentUser = await jwtService.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(jwt_service.decodeAccessToken(token)).to.be.rejectedWith( + return expect(jwtService.decodeAccessToken(token)).to.be.rejectedWith( error, ); }); + async function setupApp() { + app = await setupApplication(); + app.bind(PasswordHasherBindings.ROUNDS).to(4); + } + async function createUser() { - user.password = await hashPassword(user.password, 4); + bcryptHasher = await app.get(PasswordHasherBindings.PASSWORD_HASHER); + user.password = await bcryptHasher.hashPassword(user.password); newUser = await userRepo.create(user); } + async function clearDatabase() { await userRepo.deleteAll(); } + async function createService() { - jwt_service = new JWTAuthenticationService(userRepo, JWT_SECRET); + jwtService = await app.get(JWTAuthenticationBindings.SERVICE); } });