Skip to content

Instantly share code, notes, and snippets.

@cklanac
Last active November 3, 2018 08:52
Show Gist options
  • Save cklanac/31d648ad91cc4c5b1043609aac92b58d to your computer and use it in GitHub Desktop.
Save cklanac/31d648ad91cc4c5b1043609aac92b58d to your computer and use it in GitHub Desktop.
Challenge 16: Local Auth

Challenge - Local Authentication

In this challenge you'll add authentication using Passport Local Strategy to the Noteful app.

Requirements

Save a new User to the DB

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 User schema and model

Create a /models/user.js file and add a userSchema and User model. The schema should have the following:

  • fullname as a String
  • username as a String that is both required and unique
  • password 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.

Create a POST /api/users endpoint

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.

Update server.js

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

Create a Local Strategy and /login endpoint

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 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 both passport and passport-local in the project
  • In /passport/local.js
    • Require passport-local in the file and set the Strategy property to a local variable named LocalStrategy using object destructuring.
    • Require the User model
    • Define a new local strategy using new LocalStrategy using the skeleton code below.
  • And in /models/users.js
    • Add a validatePassword method which compare the incoming password to the plain-text password in the database.

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 protected login route

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. The failWithError 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 on server.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

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.

Add Bcrypt

Currently, the passwords are stored in plain-text, let's fix that!

Install bcryptjs and add .hash() and .compare() methods

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);
};

Update POST /users endpoint to use .hashPassword

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()

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.

Add validation to the POST /users endpoint

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 and password fields are required
  • The fields are type string
  • The username and password 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);
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment