Security and Authentication

We are continuing the todo app we did in section 7 and turning it into a more real world application.

Setting up the user models

{
email: 'email@example.com',
password: 'mypassword123'
}

It is important to remember that we cannot store passwords in a way that they show up explicitly, since a lot of users use the same passwords across all platforms.

We are going to hash the password.

Token

{
email: 'email@example.com',
password: '<hashed>',
tokens: [{
access: 'auth',
token: '<encrypted string>'
}]
}

Tokens are important for authenticating users when they take actions.

Modification of User model

Since we are validating user’s email and the email validator is complicated, we will install a third party module called validator using npm install validator --save

var User = mongoose.model('User', {
email: {
type: String,
required: true,
minlength: 1, // talk about it later
trim: true,
unique: true,
validate: {
validator: validator.isEmail,
message: `{VALUE} is not a valid email`
}
},
password: {
type: String,
require: true,
minlength: 6
},
tokens: [{
access: {
type: String,
required: true
},
token: {
type: String,
required: true
}
}]
});

Since we have added unique: true attribute to User model, we need to make sure that the MongoDB is empty and the server is shut down.

Setup POST /users route

app.post('/users', (req, res) => {
var body = _.pick(req.body, ['email', 'password']);
var user = new User(body);
user.save().then((user) => {
res.send(user);
}).catch((e) => {
res.status(400).send(e);
})
});

Try making a POST request:

Alt text

If we make the same JSON again, we will get

Alt text

Same error response will occur if the email or password is not valid.

JWTs and Hashing

Hashing

Install crypto using npm i crypto-js --save

We need to include SHA256 function from crypto-js using const {SHA256} = require('crypto-js');

Suppose we want to hash var message = 'I am user number 3';

We use the imported method

var hash = SHA256(message).toString();
console.log(`Message: ${message}`);
console.log(`Hash: ${hash}`);

To examine the result, we have designed two output clauses. The output appears:

$ node playground/hashing.js
Message: I am user number 3
Hash: 9da4d19e100809d42da806c2b7df5cf37e72623d42f1669eb112e23f5c9d45a3

This is an one-way algorithm. We can get the same result from the same String. The converse is false.

var data = {
id: 4
};
var token = {
data,
hash: SHA256(JSON.stringify(data)).toString()
};

This is not a secure way to pass the data, since users may use SHA256 to hash an object with id being 5 and send it back to us.

We need to add some salt like this:

var data = {
id: 4
};
var token = {
data,
hash: SHA256(JSON.stringify(data)+'secrete').toString()
};

To validate the token:

var resultHash = SHA256(JSON.stringify(token.data) + 'somesecrete').toString();
if (resultHash === token.hash) {
console.log('data was NOT changed');
} else {
console.log('data was changed! DO NOT trust!')
}

Since they can never get a correct pair of both value and salt-hashed value, it is secure to check if the data and the hashed version match up.

JWT

JWTs take care of what we just talked about for us, so we do not need to do the same hashing by ourself!

To install JWT, we run npm i jsonwebtoken --save.

const jwt = require('jsonwebtoken');
var data = {
id: 10
};
var token = jwt.sign(data, '123abc');
console.log(token);

This examines a basic application of hashing using JWT.

We get the result:

$ node playground/hashing.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTAsImlhdCI6MTUzNDAzOTc3Mn0.UADkHJ0OZ98l_XBmVxKbZFlmqEk-Bb3I8fQlBWH1UKg

To verify a token,

var decoded = jwt.verify(token, '123abc');
console.log('decoded', decoded);

Output:

decoded { id: 10, iat: 1534039945 }

Generating Auth Tokens and Setting Headers

Adding generateAuthToken method for every instance of User by

var UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
minlength: 1, // talk about it later
trim: true,
unique: true,
validate: {
validator: validator.isEmail,
message: `{VALUE} is not a valid email`
}
},
password: {
type: String,
require: true,
minlength: 6
},
tokens: [{
access: {
type: String,
required: true
},
token: {
type: String,
required: true
}
}]
})
UserSchema.methods.generateAuthToken = function () {
var user = this;
var access = 'auth';
var token = jwt.sign({
_id: user._id.toHexString(),
access
}, 'abc123').toString();
user.tokens.push({access, token});
return user.save().then(() => {
return token; // passed as success arg for the next `then` call
});
}
var User = mongoose.model('User', UserSchema);

Modify the route for POST user in this way:

app.post('/users', (req, res) => {
var body = _.pick(req.body, ['email', 'password']);
var user = new User(body);
user.save().then(() => {
return user.generateAuthToken();
}).then((token) => {
console.log('token:', token);
res
.header('x-auth', token) // `x-` indicates a custom header
.send(user);
})
.catch((e) => {
res.status(400).send(e);
})
});

Alt text

The problem is that we are sending tokens and password back as well, so we need to override the function now!

UserSchema.methods.toJSON = function () {
var user = this;
var userObject = user.toObject();
return _.pick(userObject, ['_id', 'email']);
};

Alt text


Rrivate routes and auth middleware

We need a method findByToken that returns a User by its token.

UserSchema.statics.findByToken = function (token) {
var User = this; // `this` binds to Model
var decoded;
try {
decoded = jwt.verify(token, 'abc123');
} catch (e) {
}
return User.findOne({
// NESTED query
"_id": decoded._id,
"tokens.token": token,
"tokens.access": 'auth'
}); // a promise returned
};

We have not handled the error case yet, but we will soon.

We can go to the route code now.

app.get('/users/me', (req, res) => {
var token = req.header('x-auth');
User.findByToken(token).then((user) => {
if (!user) {
}
res.send(user);
});
});

Now, get this x-auth token after you post a new user and try to use the token as your request header to fetch /users/me.

Alt text

Alt text

The remainder is to finish the error handling.

return new Promise((resolve, reject) => {
reject;
})

Or, simply,

return Promise.reject();

Both of these two play exactly the same role.

Final Code

UserSchema.statics.findByToken = function (token) {
var User = this; // `this` binds to Model
var decoded;
try {
decoded = jwt.verify(token, 'abc123');
} catch (e) {
// return new Promise((resolve, reject) => {
// reject();
// })
return Promise.reject();
}
return User.findOne({
// NESTED query
"_id": decoded._id,
"tokens.token": token,
"tokens.access": 'auth'
}); // a promise returned
};
app.get('/users/me', (req, res) => {
var token = req.header('x-auth');
User.findByToken(token).then((user) => {
if (!user) {
return Promise.reject;
}
res.send(user);
}).catch((e) => {
res.status(401).send();
});
});

If the token given is not valid, we will get a 401 error like this

Alt text

Simplification

var authenticate = (req, res, next) => {
var token = req.header('x-auth');
User.findByToken(token).then((user) => {
if (!user) {
return Promise.reject();
}
req.user = user;
req.token = token;
next();
}).catch((e) => {
res.status(401).send();
});
}

What does authenticate do?

To use this authentication middleware, we simply add this as the middle argument as above:

app.get('/users/me', authenticate,(req, res) => {
res.send(req.user);
});

Now to turn every route into a private one is very easy .


Hashing Passwords

To increase the security of our app, not only do we use the encryption technique we did previously, we also want to hash our passwords.

We need a third module bcryptjs, using npm i bcryptjs --save.

For documentation, you can reference https://www.npmjs.com/package/bcryptjs.

Example

const bcrypt = require('bcryptjs');
var password = '123abc!';
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(password, salt, (err, hash) => {
console.log(hash);
})
})

Output

$ node playground/hashing.js
$2a$10$1GIM8aZqkqCDD1AEpJlccOqSPiitAVSbOrqcMr0QHN4FXx/yinOba

When someone logs in, we will use the same hashing to hash their password and compare it with our result.

Example to illustrate this point would be:

var hashedPassword = '$2a$10$1GIM8aZqkqCDD1AEpJlccOqSPiitAVSbOrqcMr0QHN4FXx/yinOba';
bcrypt.compare(password, hashedPassword, (err, res) => {
console.log(res);
})

Output:

$ node playground/hashing.js
true

We need our mongoose to store hashed password. For this, we need mongoose middleware.


Seeding Test Database with Users

We create a seed.js file storing

const { ObjectID } = require('mongodb');
const { Todo } = require('./../../models/todo');
const todos = [{
_id: new ObjectID(),
text: 'first test todo'
}, {
_id: new ObjectID(),
text: 'second test todo',
completed: true,
completedAt: 333
}];
const populateTodos = (done) => {
Todo.remove({}).then(() => {
Todo.insertMany(todos)
.then(() => {
console.log('**** beforeEach successful');
done();
})
.catch((e) => {console.log('**** tests beforeEach error:', e)});
});
};
module.exports = {todos, populateTodos};

Then, in server.test.js, we simply need

const {todos, populateTodos} = require('./seed/seed');
beforeEach(populateTodos);

Then we have our todos array for testing and the function to generate these todos in the real database.

We are going to do the same procedures to the User model.

Set up the users array first, two users, one with authentication, one without:

const userOneId = new ObjectID();
const userTwoId = new ObjectID();
const users = [{
_id: userOneId,
email: 'jude@todos.com',
password: 'userOnePass',
tokens: [{
access: 'auth',
token: jwt.sign({_id: userOneId, access: 'auth'}, 'abc123').toString();
}
]
}, {
_id: userTwoId,
email: 'troy@todos.com',
password: 'userTwoPass'
}];

Now, write a similar method to populateTodos , i.e. populateUsers:

const populateUsers = (done) => {
User.remove({}).then(() => {
var userOne = new User(users[0]).save();
var userTwo = new User(users[1]).save();
return Promise.all([userOne, userTwo]); // what is this?
}).then(() => done());
};

Two highlights:

  1. We did not use insertMany, because it does not use our middleware. In particular, it does not hash our password at all.
  2. We used Promise.all. This enables us to only have then, i.e. promise only resolved, when two users are saved.

Testing POST /users and GET /users/me

describe('GET /users/me', () => {
it('should return user if authenticated', (done) => {
request(app)
.get('/users/me')
.set('x-auth', users[0].tokens[0].token)
.expect(200)
.expect((res) => {
expect(res.body._id).toBe(users[0]._id.toHexString());
expect(res.body.email).toBe(users[0].email);
})
.end(done);
});
it('should return 401 if authenticated', (done) => {
request(app)
.get('/users/me')
.expect(401)
.expect((res) => {
expect(res.body).toEqual({});
})
.end(done);
});
});
describe('POST /users/', () => {
it('should create a user', (done) => {
var email = 'example@example.com';
var password = '123mnb!';
request(app)
.post('/users')
.send({email, password})
.expect(200)
.expect((res) => {
expect(res.headers['x-auth']).toBeTruthy();
expect(res.body._id).toBeTruthy();
expect(res.body.email).toBe(email);
})
.end((err) => {
if (err) {
return done(err);
}
User.findOne({email}).then((user) => {
expect(user).toBeTruthy();
expect(user.password).not.toBe(password); // must be hashed then not equal
done();
})
})
});
it('should return validation errors if quest invalid', (done) => {
request(app)
.post('/users')
.send({
email: 'and',
password: '123'
})
.expect(400)
.end(done);
});
it('should not create user if email in use', (done) => {
request(app)
.post('/users')
.send({
email: users[0].email,
password: 'Password123!'
})
.expect(400)
.end(done);
});
})

Logging in - POST /users/login

To get the token, the threshold is limited if we only allow them to obtain one from POST /users, i.e. during registration. If we lost the token because we might want to switch to another device, we just could not have anything to do with my account. Thus, we are designing another route dedicated to logging in users.

User.findByCredentials

UserSchema.statics.findByCredentials = function (email, password) {
var User = this;
return User.findOne({email}).then((user) => {
if (!user) {
return Promise.reject();
}
// `bcrypt` does not support Promise, so we create one
return new Promise((resolve, reject) => {
// bcrypt.compare to compare password and user.password
bcrypt.compare(password, user.password, (err, res) => {
if (res) {
resolve(user);
} else {
reject();
}
});
});
});
};
app.post('/users/login', (req, res) => {
var body = _.pick(req.body, ['email', 'password']);
User.findByCredentials(body.email, body.password).then((user) => {
res.send(user);
}).catch((e) => {
res.status(400).send();
});
});

Now, if we send a JSON containing the correct username and password, we get

Alt text

However, if we send a bad password, we get 400.

Alt text

Generate the new token and send it back

Remember in the user model, we have defined a method called generateAuthToken. This is reusable!

app.post('/users/login', (req, res) => {
var body = _.pick(req.body, ['email', 'password']);
User.findByCredentials(body.email, body.password).then((user) => {
return user.generateAuthToken().then((token) => {
res.header('x-auth', token).send(user);
});
}).catch((e) => {
console.log('login error:', e);
res.status(400).send();
});
});

Testing POST /users/login

describe('POST /users/login', () => {
it('should login user and return auth token', (done) => {
request(app)
.post('/users/login')
.send({
email: users[1].email,
password: users[1].password
})
.expect(200)
.expect((res) => {
expect(res.headers['x-auth']).toBeTruthy();
})
.end((err, res) => {
if (err) {
return done(err);
}
User.findById(users[1]._id).then((user) => {
expect(user.tokens[0]).toMatchObject({
access: 'auth',
token: res.headers['x-auth']
});
done();
}).catch((e) => done(e));
});
});
it('should reject invalid login', (done) => {
request(app)
.post('/users/login')
.send({
email: users[1].email,
password: users[1].password + '1'
})
.expect(400)
.expect((res) => {
expect(res.headers['x-auth']).toBeFalsy();
})
.end((err, res) => {
if (err) {
return done(err);
}
User.findById(users[1]._id).then((user) => {
expect(user.tokens.length).toBe(0);
done();
}).catch((e) => done(e));
});
});
});

Logging Out - DELETE /users/me/token

Add a new instance method for removing token by a given token. We are going to use pull mongoose operator.

UserSchema.methods.removeToken = function (token) {
var user = this;
return user.update({
$pull: {
tokens: {
token
}
}
})
};

To log out, we need to specify this is about deletion, so it is an HTTP delete.

app.delete('/users/me/token', authenticate, (req, res) => {
req.user.removeToken(req.token).then(() => {
res.status(200).send();
}, () => {
res.status(400).send();
})
});

Alt text

Alt text

Now we saw that our token was deleted by this 200 status and we verified it through mongoDB.

Testing DELETE /users/me/token

describe('DELETE /users/me/token', () => {
it('should remove auth token on logout', (done) => {
// DELETE /users/me/token
// Set x-auth equal to token
// 200
// Find user, verify that tokens array has length of zero
request(app)
.delete('/users/me/token')
.set('x-auth', users[0].tokens[0].token)
.expect(200)
.end((err, res) => {
if (err) {
return done(err);
}
User.findById(users[0]._id).then((user) => {
expect(user.tokens.length).toBe(0);
done();
}).catch((e) => done(e));
});
});
});

Making Todo Routes Private (Part I)

We need every todo have an owner. To record their owner, we add another required field into Todo’s model, which is _creator.

_creator: { // user _ to specify that who created this
required: true,
type: mongoose.Schema.Types.ObjectId
}

To demonstrate how we use _creator, we modify the todos in the seed like this

const userOneId = new ObjectID();
const userTwoId = new ObjectID();
const users = [{
_id: userOneId,
email: 'jude@todos.com',
password: 'userOnePass',
tokens: [{
access: 'auth',
token: jwt.sign({_id: userOneId, access: 'auth'}, 'abc123').toString()
}
]
}, {
_id: userTwoId,
email: 'troy@todos.com',
password: 'userTwoPass'
}];
const todos = [{
_id: new ObjectID(),
text: 'first test todo',
_creator: userOneId
}, {
_id: new ObjectID(),
text: 'second test todo',
completed: true,
completedAt: 333,
_creator: userTwoId
}];

Whenever we create a Todo, we have to mention its owner. When we use POST /todos route, we must specify that the owner of this todo.

In particular, if the action of adding Todo is to be taken, this action must already be authorized.

To authorize the user’s actions, we add authenticate middleware.

Recall that an authenticate middleware is defined as follows:

var authenticate = (req, res, next) => {
var token = req.header('x-auth');
User.findByToken(token).then((user) => {
if (!user) {
return Promise.reject();
}
req.user = user;
req.token = token;
next();
}).catch((e) => {
res.status(401).send();
});
};

Before the private route body runs, we verify that the request has x-auth token and it is correct.

We do not necessarily have the request containing JSON file about this user, (and, even they provide one, the information may be wrong) so we search user by this token and add it to the request user field and also add the token to token field.

app.post('/todos', authenticate, (req, res) => {
console.log(req.body); // body is from body-parser
var todo = new Todo({
text: req.body.text,
_creator: req.user._id
});
todo.save().then((doc)=>{
res.send(doc);
},(e)=>{
res.status(400).send(e);
})
})

Now our Todo has the creator field, which is obtained from the authorized and thus modified request.

For GET /todos

Previously, we created the GET /todos route and we did not add any authentication to the route. Then, the query we used is simply Todo.find(), but this returns every Todo in the database.

To return the todos the given user created not for everyone, we add authentication and modify the query to Todo.find({_creator: req.user._id})

app.get('/todos', authenticate, (req, res) => {
Todo.find({
_creator: req.user._id
}).then((todos)=>{res.send({todos})},(e)=>res.status(400).send(e));
});

Because we used the old version of POST/GET /todos, our tests are going to fail.

We need to add .set('x-auth', users[0].tokens[0].token) before sending.

For GET /todos/:id

We are no longer gonna find a todo back by ID, since we don’t want other people’s todos to be found by one user.

We make the change:

app.get('/todos/:id', authenticate, (req, res) => {
var id = req.params.id;
if (!ObjectID.isValid(id)) {
return res.status(404).send();
}
Todo.findOne({
_id: id,
_creator: req.user._id
}).then((todo) => {
if (!todo) {
return res.status(404).send();
}
res.send({todo});
}).catch((e) => {
res.status(400).send();
})
});

Now we have to change the test cases, i.e. add auth-x header before sending the test request.

Also, we add another test:

it('should not return todo created by other user', (done) => {
request(app)
.get(`/todos/${todos[1]._id.toHexString()}`)
.set('x-auth', users[0].tokens[0].token)
.expect(404)
.end(done);
});

For DELETE /todos/:id

app.delete('/todos/:id', authenticate, (req, res) => {
// get the id
var id = req.params.id;
// validate the id -> not valid? return 404
if (!ObjectID.isValid(id)) {
return res.status(404).send();
}
// remove todo by id
Todo.findOneRemove({
_id: id,
_creator: req.user._id
}).then((todo) => {
if (!todo) {
return res.status(404).send();
}
res.send({todo});
}).catch((e) => {
console.log(e);
res.status(400).send();
})
})
it('should remove a todo', (done) => {
var hexId = todos[0]._id.toHexString();
request(app)
.delete(`/todos/${hexId}`)
.set('x-auth', users[1].tokens[0].token)
.expect(404)
.end((err, res) => {
// query db using findByIdAndRemove toNotExist
// expect(null).toNotExist()
if (err) {
return done(err);
}
Todo.findById(hexId).then((todo) => {
expect(todo).toBeTruthy(); // since we could never remove it
done();
}).catch((e) => done(e));
});
});

For PATCH /todos/:id

app.patch('/todos/:id', authenticate, (req, res) => {
var id = req.params.id;
var body = _.pick(req.body, ['text', 'completed']);
if (!ObjectID.isValid(id)) {
return res.status(404).send();
}
if (_.isBoolean(body.completed) && body.completed) {
body.completedAt = new Date().getTime(); // time stamp
} else {
body.completed = false;
body.completedAt = null;
}
Todo.findOneAndUpdate({
_id: id,
_creator: req.user._id
}, {$set: body}, {new: true}).then((todo) => {
if (!todo) {
return res.status(404).send();
}
res.send({todo});
}).catch((e) => {
res.status(400).send();
});
})

Test suite:

describe('PATCH /todos/:id', () => {
it('should update the todo', (done) => {
var hexId = todos[0]._id.toHexString();
var text = "This should be the new text";
request(app)
.patch(`/todos/${hexId}`)
.set('x-auth', users[0].tokens[0].token)
.send({
completed: true,
text
})
.expect(200)
.expect((res) => {
expect(res.body.todo.text).toBe(text);
expect(res.body.todo.completed).toBe(true);
expect(typeof res.body.todo.completedAt).toBe('number');
})
.end(done);
});
it('should not update the todo created by other user', (done) => {
var hexId = todos[0]._id.toHexString();
var text = "This should be the new text";
request(app)
.patch(`/todos/${hexId}`)
.set('x-auth', users[1].tokens[0].token)
.send({
completed: true,
text
})
.expect(404)
.end(done);
})
it('should clear completedAt when todo is not completed', (done) => {
var hexId = todos[1]._id.toHexString();
var text = "This should be the new text!!";
request(app)
.patch(`/todos/${hexId}`)
.set('x-auth', users[1].tokens[0].token)
.send({
completed: false,
text
})
.expect(200)
.expect((res) => {
expect(res.body.todo.text).toBe(text);
expect(res.body.todo.completed).toBe(false);
expect(res.body.todo.completedAt).toBeNull();
})
.end(done);
});
});

Improving App Configuration

We do not want some important key-value pairs public. The strategy we are going to use is create a JSON file that is not pushed into the GIT repo, and we store configs in the file.

Example

{
"test": {
"PORT": 8000,
"MONGODB_URI": "mongodb://localhost:27017/TodoAppTest"
},
"development": {
"PORT": 8000,
"MONGODB_URI": "mongodb://localhost:27017/TodoApp"
}
}

To use this JSON file in config.js, we do not need to use JSON.parse(), but simply require it.

if (env === 'development' || env === 'test') {
var config = require('./config.json');
console.log(config);
}

We could see that the output is correct.

{ test:
{ PORT: 8000,
MONGODB_URI: 'mongodb://localhost:27017/TodoAppTest' },
development: { PORT: 8000, MONGODB_URI: 'mongodb://localhost:27017/TodoApp' } }

To assign those values from JSON file to process.env, we use Object.keys

if (env === 'development' || env === 'test') {
var config = require('./config.json');
var envConfig = config[env];
Object.keys(envConfig).forEach((key) => {
process.env[key] = envConfig[key];
}); // return an array containing all the keys
}

How to set this JWT_SECRET for HEROKU?

heroku config shows the environment variables set by HEROKU.

heroku config:set KEY=VALUE sets environment key-value pair.

heroku config:get KEY fetches environment variable value by key

heroku config:unset KEY unsets environment variable value by key

Deploying to Heroku

Understand the structure of MongoDB connection string:

mongodb://[USER_NAME]:[PASSWORD]@[ADDRESS]:[PORT]/[DATABASE]

Connect to Remote Database

Alt text

Alt text