Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: bcrypt hash #51

Merged
merged 1 commit into from
Feb 21, 2019
Merged

refactor: bcrypt hash #51

merged 1 commit into from
Feb 21, 2019

Conversation

jannyHou
Copy link
Contributor

@jannyHou jannyHou commented Feb 15, 2019

A follow up PR for #33

connect to #40

Description

  • Refactor bcrypt hashing related functions into a service.
  • Decouple the password hashing service from JWT service.

Details see discussion in #33 (comment)

@jannyHou jannyHou force-pushed the refactor/bcrypt-hash branch 2 times, most recently from 71e9754 to 671d8cb Compare February 15, 2019 04:03
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about the architecture of the tests and why are they calling the hasher function directly. Feel free to leave this discussion for later, after the PR is landed.

I have few comments about the new bindings, PTAL.

src/keys.ts Outdated
'services.hash_password',
export namespace BcryptHasherBindings {
export const BCRYPT_HASHER = BindingKey.create<BcryptHasher>(
'services.bcrypt.hasher',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In dependency inversion, the bindings are usually defined via interfaces, to allow consumers to provide any implementation matching the required contract.

I am proposing to change this binding to something like

export const PASSWORD_HASHER = BindingKey.create<PasswordHasher>(
  'services.password-hasher'
);


export interface PasswordHasher<T = string> {
hashPassword(password: T): Promise<T>;
comparePassword(provided_pass: T, stored_pass: T): Promise<boolean>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use camelCase, not snake_case.

Suggested change
comparePassword(provided_pass: T, stored_pass: T): Promise<boolean>;
comparePassword(providedPass: T, storedPass: T): Promise<boolean>;


async comparePassword(
provided_pass: string,
stored_pass: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

src/keys.ts Outdated
export const BCRYPT_HASHER = BindingKey.create<BcryptHasher>(
'services.bcrypt.hasher',
);
export const ROUND = BindingKey.create<number>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const ROUND = BindingKey.create<number>(
export const ROUNDS = BindingKey.create<number>(

(plural "rounds", not a singular "round")


before('create new user', async () => {
bcrypt_hasher = new BcryptHasher(4);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is acceptance-level test, behavior should be configured mostly at app level via bindings.

I think you should change BcryptHasherBindings.ROUNDS in one of the early hooks to ensure that all tests are using less rounds to hash password when creating new users.

plainPassword = user.password;
user.password = await hashPassword(user.password, 4);
jwt_auth_service = new JWTAuthenticationService(userRepo, JWT_SECRET);
user.password = await bcrypt_hasher.hashPassword(user.password);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about this. Why is it necessary to hash the password here? Shouldn't UserRepository take care of hashing the password? At the moment, it's too easy to forget to hash the password before calling userRepo.create and end up with an invalid user record.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos The reason is the "hashing password" logic is handled in the user controller, NOT user repository.
When we create the user by making client request POST /Users, the controller will execute the hashing logic to make sure the hashed password is stored.
While when we call userRepo.create(user) directly, as what we did in the acceptance test, we need to hash the plain password first then store it.

@@ -84,14 +85,19 @@ describe('authentication utilities', () => {
});

async function createUser() {
user.password = await hashPassword(user.password, 4);
bcrypt_hasher = new BcryptHasher(4);
user.password = await bcrypt_hasher.hashPassword(user.password);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here. I would expect that the logic of password hashing should be implemented in a shared piece of code that's called both by runtime parts (repositories/controllers/services) and by tests.

Maybe I am missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jannyHou jannyHou force-pushed the refactor/bcrypt-hash branch 2 times, most recently from d433eb2 to 9920931 Compare February 19, 2019 17:12
@jannyHou
Copy link
Contributor Author

@bajtos Thank you for the review.

In dependency inversion, the bindings are usually defined via interfaces, to allow consumers to provide any implementation matching the required contract.

True I realized the implemented class is over used in most places, so I replaced it with interface PasswordHasher

And regarding your concern about the test case, see my comment in #51 (comment).

PTAL thanks.

storedPass: string,
): Promise<boolean> {
const passwordIsMatched = await compare(providedPass, storedPass);
return passwordIsMatched;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: can we do return await compare(providedPass, storedPass) here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@b-admike Sorry forgot to apply this change, I will remember to do it when refactor this example to use new authentication module.

) {}

@post('/users')
async create(@requestBody() user: User): Promise<User> {
validateCredentials(_.pick(user, ['email', 'password']));
user.password = await this.hashPassword(user.password, 10);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to set the rounds binding key to 10 for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/keys.ts Outdated
'services.hash_password',
export namespace PasswordHasherBindings {
export const PASSWORD_HASHER = BindingKey.create<PasswordHasher>(
'services.password.hasher',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Is it possible to rename services.password.hasher to services.hasher.password so that it's in the namespace hasher like services.hasher.round below?

Copy link
Contributor Author

@jannyHou jannyHou Feb 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is services.password.hasher maps to the hasher class, while services.hasher.rounds maps to a class member.

Maybe I should rename services.password.hasher to be services.hasher, does it make more sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your proposal sounds great, but feel free to ignore.

Copy link
Contributor

@b-admike b-admike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; feel free to wait for @bajtos as he might have more comments.

Signed-off-by: jannyHou <[email protected]>
@jannyHou jannyHou force-pushed the refactor/bcrypt-hash branch from 9920931 to 7e9095a Compare February 21, 2019 17:36
@jannyHou jannyHou merged commit 2860083 into master Feb 21, 2019
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@bajtos bajtos deleted the refactor/bcrypt-hash branch February 26, 2019 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants