In this challenge you'll add authentication using Passport Local Strategy to the Noteful app.
The first challenge is to add the ability to save a user to the database. This means you need to create a User schema and User model, and you need to add a POST /api/users
endpoint to create the new user.
Create a /models/user.js
file and add a userSchema
and User
model. The schema should have the following:
fullname
as a Stringusername
as a String that is both required and uniquepassword
as a String that is required
Like the previous challenges, we'll use the mongoose transform
function to modify the results from the database and create a representation. But this time you also need to prevent the password from being returned, so add a delete ret.password;
statement to userSchema.set('toObject'...)
like the following.
userSchema.set('toObject', {
virtuals: true, // include built-in virtual `id`
versionKey: false, // remove `__v` version key
transform: (doc, ret) => {
delete ret._id; // delete `_id`
delete ret.password;
}
});
Note, the password needs to be selectable in order to be compared with the incoming password during authentication, so don't use
select: false
in the schema. It will not work in this scenario.
Remember to export the model so it can be used on in other files.
Next you need to create an endpoint to insert users.
Create a /routes/users.js
file with a single POST endpoint. The endpoint creates a new user in the database and responds with a 201 status, a location header and a JSON representation of the user without the password.
Note, you will add input validation later in the challenge, after adding Bcryptjs. For now, focus on creating the ability to insert a new user.
Finally, you need to mount the new /users
route on /api
base path.
Update server.js
to require the /users
route.
const usersRouter = require('./routes/users');
And mount the route on the /api
path.
app.use('/api', usersRouter);
Test the new User
model and /api/users
route using Postman. Verify new users are saved to the database
Now that you have users we can create a /login
endpoint which check the incoming username and password against the database and conditionally allows the user access or returns an unauthorized error.
First, you will need to create a Passport Local Strategy, then you can create a /login
endpoint which is protected by the strategy.
Create a /passport/local.js
directory and file. In the file, create Passport Local Strategy that finds the user and validates the password. For now you will compare the plain-text passwords, later you will had bcryptjs to hash and compare the password.
Below are the basic steps followed by example code to get you started.
- First,
npm install
bothpassport
andpassport-local
in the project - In
/passport/local.js
- Require
passport-local
in the file and set theStrategy
property to a local variable namedLocalStrategy
using object destructuring. - Require the
User
model - Define a new local strategy using
new LocalStrategy
using the skeleton code below.
- Require
- And in
/models/users.js
- Add a
validatePassword
method which compare the incoming password to the plain-text password in the database.
- Add a
Example code for /passport/local.js
const { Strategy: LocalStrategy } = require('passport-local');
const User = require('../models/user');
// ===== Define and create basicStrategy =====
const localStrategy = new LocalStrategy((username, password, done) => {
let user;
User.findOne({ username })
.then(results => {
user = results;
if (!user) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username',
location: 'username'
});
}
const isValid = user.validatePassword(password);
if (!isValid) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect password',
location: 'password'
});
}
return done(null, user);
})
.catch(err => {
if (err.reason === 'LoginError') {
return done(null, false);
}
return done(err);
});
});
Add the following to /models/users.js
userSchema.methods.validatePassword = function (password) {
return password === this.password;
};
Again, this code assumes a plain-text password and
.validatePassword
is synchronous. You will update the.validatePassword
method to an async hashing process in a later step.
Create a /routes/auth.js
file. In the file create a POST /login
route which is protected by the local strategy you just created. Below is skeleton code but you will need to setup the correct require and export statements.
Note: the following example uses
failWithError: true
which is unique to the Noteful app. ThefailWithError
option configures the middleware to throw an error instead of automatically returning a 401 response. The error is then caught by the error handling middleware onserver.js
and returned as JSON.
const options = {session: false, failWithError: true};
const localAuth = passport.authenticate('local', options);
router.post('/login', localAuth, function (req, res) {
return res.json(req.user);
});
Update server.js
to require the new strategy and login
router. Configure passport to utilize the strategy and mount the router.
Towards the top of the file, add the require statements
const passport = require('passport');
const localStrategy = require('./passport/local');
// Other statements removed for brevity
const authRouter = require('./routes/auth');
Configure Passport to utilize the strategy
passport.use(localStrategy);
Then mount the new router along with the existing routers
app.use('/api', authRouter);
Test the new /login
route using Postman. Verify you can create a new user and then login.
Currently, the passwords are stored in plain-text, let's fix that!
First, npm install bcryptjs
. Please use bcryptjs
and not bcrypt
.
Add the .hashPassword
and update .validatePassword
methods to the User Schema in the /models/user.js
file. Remember to require bcrypt
in the file.
userSchema.methods.validatePassword = function (password) {
return bcrypt.compare(password, this.password);
};
userSchema.statics.hashPassword = function (password) {
return bcrypt.hash(password, 10);
};
Next, update the POST /users
endpoint to use .hashPassword
method. You must first call .hashPassword()
to create the digest (aka hash). Since the hashing process returns a promise, you can chain the calls using .then()
.
return User.hashPassword(password)
.then(digest => {
const newUser = {
username,
password: digest,
fullname
};
return User.create(newUser);
})
.then(result => {
return res.status(201).location(`/api/users/${result.id}`).json(result);
})
.catch(err => {
if (err.code === 11000) {
err = new Error('The username already exists');
err.status = 400;
}
next(err);
});
Update the Local Strategy to use .validatePassword()
.
let user;
User.findOne({ username })
.then(results => {
user = results;
if (!user) {
// Removed for brevity
}
return user.validatePassword(password);
})
.then(isValid => {
if (!isValid) {
// Removed for brevity
}
return done(null, user);
})
.catch(err => {
// Removed for brevity
});
Note: Remember to drop the
users
collection or delete users that have plain-text password.
Test the process using Postman:
- Register: Create a new user by posting to the
/api/users
endpoint - Inspect the database to ensure the password was hashed before saving
- Login: Post the username and password to
/api/login
, you should receive a representation of the user without the password.
The API currently works but users can enter simple passwords with only two or three characters. Add validation before persisting the user to the DB which performs the following checks.
- The
username
andpassword
fields are required - The fields are type string
- The
username
andpassword
should not have leading or trailing whitespace. And the endpoint should not automatically trim the values - The
username
is a minimum of 1 character - The
password
is a minimum of 8 and max of 72 characters
Here is an example for the first validation is provided to help get you started.
const requiredFields = ['username', 'password'];
const missingField = requiredFields.find(field => !(field in req.body));
if (missingField) {
const err = new Error(`Missing '${missingField}' in request body`);
err.status = 422;
return next(err);
}