Skip to content

Commit

Permalink
feat: extract local credentials into a new model
Browse files Browse the repository at this point in the history
Introduce `UserCredentials` models to hold hashed passwords, add has-one
relation from `User` to `UserCredentials`.

Rework authentication-related code to work with the new domain model.

Signed-off-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
bajtos committed Nov 14, 2019
1 parent ded04cf commit 19d6c92
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ import {
import {setupApplication} from './helper';
import {TokenService, UserService} from '@loopback/authentication';
import {securityId} from '@loopback/security';
import * as _ from 'lodash';

describe('authentication services', () => {
let app: ShoppingApplication;

const user = {
const userData = {
email: '[email protected]',
password: 'p4ssw0rd',
firstName: 'unit',
lastName: 'test',
};

const userPassword = 'p4ssw0rd';

let newUser: User;
let jwtService: TokenService;
let userService: UserService<User, Credentials>;
Expand Down Expand Up @@ -70,19 +72,15 @@ describe('authentication services', () => {

it('user service verifyCredentials() succeeds', async () => {
const {email} = newUser;
const credentials = {email, password: user.password};
const credentials = {email, password: userPassword};

const returnedUser = await userService.verifyCredentials(credentials);

// create a copy of returned user without password field
const returnedUserWithOutPassword = Object.assign({}, returnedUser, {
password: user.password,
});
delete returnedUserWithOutPassword.password;
const returnedUserWithOutPassword = _.omit(returnedUser, 'password');

// create a copy of expected user without password field
const expectedUserWithoutPassword = Object.assign({}, newUser);
delete expectedUserWithoutPassword.password;
const expectedUserWithoutPassword = _.omit(newUser, 'password');

expect(returnedUserWithOutPassword).to.deepEqual(
expectedUserWithoutPassword,
Expand Down Expand Up @@ -178,21 +176,21 @@ describe('authentication services', () => {
});

it('password encrypter hashPassword() succeeds', async () => {
const encrypedPassword = await bcryptHasher.hashPassword(user.password);
expect(encrypedPassword).to.not.equal(user.password);
const encrypedPassword = await bcryptHasher.hashPassword(userPassword);
expect(encrypedPassword).to.not.equal(userPassword);
});

it('password encrypter compare() succeeds', async () => {
const encrypedPassword = await bcryptHasher.hashPassword(user.password);
const encrypedPassword = await bcryptHasher.hashPassword(userPassword);
const passwordsAreTheSame = await bcryptHasher.comparePassword(
user.password,
userPassword,
encrypedPassword,
);
expect(passwordsAreTheSame).to.be.True();
});

it('password encrypter compare() fails', async () => {
const encrypedPassword = await bcryptHasher.hashPassword(user.password);
const encrypedPassword = await bcryptHasher.hashPassword(userPassword);
const passwordsAreTheSame = await bcryptHasher.comparePassword(
'someotherpassword',
encrypedPassword,
Expand All @@ -208,12 +206,14 @@ describe('authentication services', () => {

async function createUser() {
bcryptHasher = await app.get(PasswordHasherBindings.PASSWORD_HASHER);
const encryptedPassword = await bcryptHasher.hashPassword(user.password);
newUser = await userRepo.create(
Object.assign({}, user, {password: encryptedPassword}),
);
const encryptedPassword = await bcryptHasher.hashPassword(userPassword);
newUser = await userRepo.create(userData);
// MongoDB returns an id object we need to convert to string
newUser.id = newUser.id.toString();

await userRepo.userCredentials(newUser.id).create({
password: encryptedPassword,
});
}

async function clearDatabase() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ describe('authorization', () => {
let client: Client;
let userRepo: UserRepository;

let user = {
let userData = {
email: '[email protected]',
password: 'p4ssw0rd',
firstName: 'customer_service',
};

const userPassword = 'p4ssw0rd';

let passwordHasher: PasswordHasher;
let newUser: User;
let token: string;
Expand Down Expand Up @@ -54,7 +55,7 @@ describe('authorization', () => {

let res = await client
.post('/users/login')
.send({email: newUser.email, password: user.password})
.send({email: newUser.email, password: userPassword})
.expect(200);

token = res.body.token;
Expand Down Expand Up @@ -82,9 +83,8 @@ describe('authorization', () => {

describe('bob', () => {
it('allows bob create orders', async () => {
user = {
userData = {
email: '[email protected]',
password: 'p4ssw0rd',
firstName: 'bob',
};
newUser = await createAUser();
Expand All @@ -102,7 +102,7 @@ describe('authorization', () => {

let res = await client
.post('/users/login')
.send({email: newUser.email, password: user.password})
.send({email: newUser.email, password: userPassword})
.expect(200);

token = res.body.token;
Expand Down Expand Up @@ -145,13 +145,15 @@ describe('authorization', () => {
}

async function createAUser() {
const encryptedPassword = await passwordHasher.hashPassword(user.password);
const aUser = await userRepo.create(
Object.assign({}, user, {password: encryptedPassword}),
);
const encryptedPassword = await passwordHasher.hashPassword(userPassword);
const aUser = await userRepo.create(userData);

// MongoDB returns an id object we need to convert to string
aUser.id = aUser.id.toString();

await userRepo.userCredentials(aUser.id).create({
password: encryptedPassword,
});
return aUser;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ describe('UserOrderController acceptance tests', () => {

const userData = {
email: '[email protected]',
password: 'p4ssw0rd',
firstName: 'customer_service',
};

const userPassword = 'p4ssw0rd';

before('setupApplication', async () => {
({app, client} = await setupApplication());
});
Expand Down Expand Up @@ -158,22 +159,24 @@ describe('UserOrderController acceptance tests', () => {
const passwordHasher = await app.get(
PasswordHasherBindings.PASSWORD_HASHER,
);
const encryptedPassword = await passwordHasher.hashPassword(
userData.password,
);
const encryptedPassword = await passwordHasher.hashPassword(userPassword);

const newUser = await userRepo.create(userData);

const newUser = await userRepo.create(
Object.assign({}, userData, {password: encryptedPassword}),
);
// MongoDB returns an id object we need to convert to string
newUser.id = newUser.id.toString();

await userRepo.userCredentials(newUser.id).create({
password: encryptedPassword,
});

return newUser;
}

async function authenticateUser(user: User) {
const res = await client
.post('/users/login')
.send({email: user.email, password: userData.password});
.send({email: user.email, password: userPassword});

const token = res.body.token;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ describe('UserController', () => {

let userRepo: UserRepository;

const user = {
const userData = {
email: '[email protected]',
password: 'p4ssw0rd',
firstName: 'Example',
lastName: 'User',
};

const userPassword = 'p4ssw0rd';

let passwordHasher: PasswordHasher;
let expiredToken: string;

Expand All @@ -54,7 +55,7 @@ describe('UserController', () => {
it('creates new user when POST /users is invoked', async () => {
const res = await client
.post('/users')
.send(user)
.send({...userData, password: userPassword})
.expect(200);

// Assertions
Expand Down Expand Up @@ -122,19 +123,18 @@ describe('UserController', () => {
it('throws error for POST /users with an existing email', async () => {
await client
.post('/users')
.send(user)
.send({...userData, password: userPassword})
.expect(200);
const res = await client
.post('/users')
.send(user)
.send({...userData, password: userPassword})
.expect(409);

expect(res.body.error.message).to.equal('Email value is already taken');
});

it('returns a user with given id when GET /users/{id} is invoked', async () => {
const newUser = await createAUser();
delete newUser.password;
delete newUser.orders;

await client.get(`/users/${newUser.id}`).expect(200, newUser.toJSON());
Expand All @@ -146,7 +146,7 @@ describe('UserController', () => {

const res = await client
.post('/users/login')
.send({email: newUser.email, password: user.password})
.send({email: newUser.email, password: userPassword})
.expect(200);

const token = res.body.token;
Expand All @@ -158,7 +158,7 @@ describe('UserController', () => {

const res = await client
.post('/users/login')
.send({email: '[email protected]', password: user.password})
.send({email: '[email protected]', password: userPassword})
.expect(401);

expect(res.body.error.message).to.equal('Invalid email or password.');
Expand All @@ -180,7 +180,7 @@ describe('UserController', () => {

let res = await client
.post('/users/login')
.send({email: newUser.email, password: user.password})
.send({email: newUser.email, password: userPassword})
.expect(200);

const token = res.body.token;
Expand Down Expand Up @@ -280,13 +280,15 @@ describe('UserController', () => {
}

async function createAUser() {
const encryptedPassword = await passwordHasher.hashPassword(user.password);
const newUser = await userRepo.create(
Object.assign({}, user, {password: encryptedPassword}),
);
const encryptedPassword = await passwordHasher.hashPassword(userPassword);
const newUser = await userRepo.create(userData);
// MongoDB returns an id object we need to convert to string
newUser.id = newUser.id.toString();

await userRepo.userCredentials(newUser.id).create({
password: encryptedPassword,
});

return newUser;
}

Expand Down
55 changes: 44 additions & 11 deletions packages/shopping/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {repository} from '@loopback/repository';
import {repository, model, property} from '@loopback/repository';
import {validateCredentials} from '../services/validator';
import {post, param, get, requestBody, HttpErrors} from '@loopback/rest';
import {
post,
param,
get,
requestBody,
HttpErrors,
getModelSchemaRef,
} from '@loopback/rest';
import {User, Product} from '../models';
import {UserRepository} from '../repositories';
import {RecommenderService} from '../services/recommender.service';
Expand All @@ -31,6 +38,15 @@ import {
import * as _ from 'lodash';
import {OPERATION_SECURITY_SPEC} from '../utils/security-spec';

@model()
export class NewUserRequest extends User {
@property({
type: 'string',
required: true,
})
password: string;
}

export class UserController {
constructor(
@repository(UserRepository) public userRepository: UserRepository,
Expand Down Expand Up @@ -58,18 +74,37 @@ export class UserController {
},
},
})
async create(@requestBody() user: User): Promise<User> {
async create(
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(NewUserRequest, {
title: 'NewUser',
exclude: ['id'],
}),
},
},
})
newUserRequest: Omit<NewUserRequest, 'id'>,
): Promise<User> {
// ensure a valid email value and password value
validateCredentials(_.pick(user, ['email', 'password']));
validateCredentials(_.pick(newUserRequest, ['email', 'password']));

// encrypt the password
// eslint-disable-next-line require-atomic-updates
user.password = await this.passwordHasher.hashPassword(user.password);
const password = await this.passwordHasher.hashPassword(
newUserRequest.password,
);

try {
// create the new user
const savedUser = await this.userRepository.create(user);
delete savedUser.password;
const savedUser = await this.userRepository.create(
_.omit(newUserRequest, 'password'),
);

// set the password
await this.userRepository
.userCredentials(savedUser.id)
.create({password});

return savedUser;
} catch (error) {
Expand Down Expand Up @@ -97,9 +132,7 @@ export class UserController {
},
})
async findById(@param.path.string('userId') userId: string): Promise<User> {
return this.userRepository.findById(userId, {
fields: {password: false},
});
return this.userRepository.findById(userId);
}

@get('/users/me', {
Expand Down
1 change: 1 addition & 0 deletions packages/shopping/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './shopping-cart-item.model';
export * from './shopping-cart.model';
export * from './order.model';
export * from './product.model';
export * from './user-credentials.model';
Loading

0 comments on commit 19d6c92

Please sign in to comment.