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 26, 2019
1 parent 880dd72 commit 6c3764c
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 78 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ http://[::1]:3000/explorer/.

## Models

This app has five models:
This app has the following models:

1. `User` - representing the users of the system.
2. `Product` - a model which is mapped to a remote service by
2. `UserCredentials` - representing sensitive credentials like a password.
3. `Product` - a model which is mapped to a remote service by
`services/recommender.service`.
3. `ShoppingCartItem` - a model for representing purchases.
4. `ShoppingCart` - a model to represent a user's shopping cart, can contain
4. `ShoppingCartItem` - a model for representing purchases.
5. `ShoppingCart` - a model to represent a user's shopping cart, can contain
many items (`items`) of the type `ShoppingCartItem`.
5. `Order` - a model to represent an order by user, can have many products
6. `Order` - a model to represent an order by user, can have many products
(`products`) of the type `ShoppingCartItem`.

`ShoppingCart` and `Order` are marked as belonging to the `User` model by the
Expand All @@ -50,6 +51,10 @@ marked as having many `Order`s using the `@hasMany` model decorator. Although
possible, a `hasMany` relation for `User` to `ShoppingCart` has not be created
in this particular app to limit the scope of the example.

`User` is also marked as having one `UserCredentials` model using the `@hasOne`
decorator. The `belongsTo` relation for `UserCredentials` to `User` has not been
created to keep the scope smaller.

## Controllers

Controllers expose API endpoints for interacting with the models and more.
Expand Down
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 All @@ -65,6 +66,20 @@ describe('UserController', () => {
expect(res.body).to.not.have.property('password');
});

it('creates a new user with the given id', async () => {
// This test verifies the scenario described in our docs, see
// https://loopback.io/doc/en/lb4/Authentication-Tutorial.html
const res = await client.post('/users').send({
id: '5dd6acee242760334f6aef65',
...userData,
password: userPassword,
});
expect(res.body).to.deepEqual({
id: '5dd6acee242760334f6aef65',
...userData,
});
});

it('throws error for POST /users with a missing email', async () => {
const res = await client
.post('/users')
Expand Down Expand Up @@ -122,19 +137,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 +160,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 +172,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 +194,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 +294,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
Loading

0 comments on commit 6c3764c

Please sign in to comment.