Skip to content

Commit

Permalink
refactor: bcrypt hash
Browse files Browse the repository at this point in the history
Signed-off-by: jannyHou <[email protected]>
  • Loading branch information
jannyHou authored and Janny committed Feb 21, 2019
1 parent 1ffc806 commit 2860083
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 43 deletions.
9 changes: 5 additions & 4 deletions src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 8 additions & 8 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -39,16 +39,16 @@ export class UserController {
public recommender: RecommenderService,
@inject.setter(AuthenticationBindings.CURRENT_USER)
public setCurrentUser: Setter<UserProfile>,
@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<User> {
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);
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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};
Expand Down
9 changes: 5 additions & 4 deletions src/keys.ts
Original file line number Diff line number Diff line change
@@ -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?
Expand All @@ -15,8 +15,9 @@ export namespace JWTAuthenticationBindings {
);
}

export namespace OtherServicesBindings {
export const HASH_PASSWORD = BindingKey.create<HashPassword>(
'services.hash_password',
export namespace PasswordHasherBindings {
export const PASSWORD_HASHER = BindingKey.create<PasswordHasher>(
'services.hasher',
);
export const ROUNDS = BindingKey.create<number>('services.hasher.round');
}
10 changes: 7 additions & 3 deletions src/services/JWT.authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
) {}

/**
Expand All @@ -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.');
}
Expand Down
28 changes: 28 additions & 0 deletions src/services/hash.password.bcryptjs.ts
Original file line number Diff line number Diff line change
@@ -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'.
Expand All @@ -17,3 +20,28 @@ export async function hashPassword(
const salt = await genSalt(rounds);
return await hash(password, salt);
}

export interface PasswordHasher<T = string> {
hashPassword(password: T): Promise<T>;
comparePassword(providedPass: T, storedPass: T): Promise<boolean>;
}

export class BcryptHasher implements PasswordHasher<string> {
constructor(
@inject(PasswordHasherBindings.ROUNDS)
private readonly rounds: number,
) {}

async hashPassword(password: string): Promise<string> {
const salt = await genSalt(this.rounds);
return await hash(password, salt);
}

async comparePassword(
providedPass: string,
storedPass: string,
): Promise<boolean> {
const passwordIsMatched = await compare(providedPass, storedPass);
return passwordIsMatched;
}
}
23 changes: 14 additions & 9 deletions test/acceptance/user.controller.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
});
Expand All @@ -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,
});
Expand Down
18 changes: 18 additions & 0 deletions test/unit/helper.ts
Original file line number Diff line number Diff line change
@@ -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<ShoppingApplication> {
const app = new ShoppingApplication({
rest: givenHttpServerConfig(),
});

await app.boot();
await app.start();

return app;
}
46 changes: 31 additions & 15 deletions test/unit/utils.authentication.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,44 @@

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: '[email protected]',
password: 'p4ssw0rd',
firstname: 'unit',
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: '[email protected]',
password: 'p4ssw0rd',
});
Expand All @@ -46,7 +54,7 @@ describe('authentication utilities', () => {
`User with email [email protected] not found.`,
);
return expect(
jwt_service.getAccessTokenForUser({
jwtService.getAccessTokenForUser({
email: '[email protected]',
password: 'fake',
}),
Expand All @@ -58,40 +66,48 @@ describe('authentication utilities', () => {
'The credentials are not correct.',
);
return expect(
jwt_service.getAccessTokenForUser({
jwtService.getAccessTokenForUser({
email: '[email protected]',
password: 'fake',
}),
).to.be.rejectedWith(expectedError);
});

it('decodeAccessToken decodes valid access token', async () => {
const token = await jwt_service.getAccessTokenForUser({
const token = await jwtService.getAccessTokenForUser({
email: '[email protected]',
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);
}
});

Expand Down

0 comments on commit 2860083

Please sign in to comment.