Lesson 25. Capstone: Adding User Authentication to Confetti Cuisine

My contacts at Confetti Cuisine are delighted with the progress on their application. They’ve already started to add new course offerings, manage new subscribers, and spread the word about creating new user accounts. I warn them that although user accounts can be created, the application isn’t ready to handle users securely.

The client and I agree that data encryption and proper user authentication are the way forward, so for my next improvements to the application, I’m going to add a couple of packages that use Passport.js to assist in setting up a secure user-login process. I’ll also add flash messaging so that users can tell after a redirect or page render whether their last operation was successful. Then I’ll add some additional validations with the help of the express-validator middleware package.

By the end of this stage of development, I can comfortably encourage Confetti Cuisine to sign users up for their application. Because the application isn’t yet live online, though, the client will have to run it locally on their machines when users sign up.

For this capstone exercise, I’ll need to do the following:

  • Add sessions and cookies between page requests
  • Add new custom middleware for validations and setting up local variables in the views
  • Create a login form
  • Add passport authentication and encryption for the User model
  • Add a visual indicator to show which user is logged in

25.1. Getting set up

Working off the code I wrote in the last capstone exercise (lesson 21), I currently have three models implemented with CRUD actions for each. To move forward with the improvements to Confetti Cuisine’s application, I need to install a few more packages:

  • express-session allows me to store temporary data about the user interaction with the application. The resulting sessions let me know whether a user has logged in recently.
  • cookie-parser allows me to store session data on the client. The resulting cookies are sent with each request and response, carrying within them messages and data reflecting the user who last used that client.
  • connect-flash allows me to use sessions and cookies to generate flash messages in the user’s browser.
  • express-validator allows me to add a layer of validations to incoming user data through a middleware function.
  • passport allows me to set up a painless encryption and authentication process for the User model.
  • passport-local-mongoose allows me to integrate passport even further by simplifying the code I need to write through a plugin I can use on the User model.

To install these packages, I’ll run npm i express-session cookie-parser connect-flash express-validator passport passport-local-mongoose -S in my projects terminal window. I’ve already set up the create action and new form for users. I need to modify those soon, but first, I’ll create the login form needed for users to log in to the application.

25.2. Creating a login form

I want this form to contain two straightforward inputs: email and password. I’ll create a new login.ejs view in the users folder and add the code in the next listing. This form will submit a POST request to the /users/login route. The inputs of this form will handle the user’s email and password.

Listing 25.1. Adding a login form to users/login.ejs
<form class="form-signin" action="/users/login" method="POST">     1
  <h2 class="form-signin-heading">Login:</h2>
  <label for="inputEmail" class="sr-only">Email</label>
  <input type="text" name="email" id="inputEmail" class="form-
 control" placeholder="Email" autofocus required>
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" name="password" id="inputPassword"
 class="form-control" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">
 Login</button>
</form>

  • 1 Create a login form.

Before this form can work or be viewed, I’ll add the login routes and actions. The login will accept GET and POST requests, as shown in the following listing.

Note

I add all routing-specific code on the router object.

Listing 25.2. Adding a login route to main.js
router.get("/users/login", usersController.login);              1
router.post("/users/login", usersController.authenticate);      2
router.get("/users/logout", usersController.logout,
  usersController.redirectView );                            3

  • 1 Route to the login action.
  • 2 Send posted data to an authenticate action.
  • 3 Add a route to logout and redirect to a view.

With these routes in place, I need to create their corresponding actions before my form is viewable at /users/login. First, I’ll add the login action from the next listing to users-Controller.js.

Listing 25.3. Adding the login action to usersController.js
login: (req, res) => {
  res.render("users/login");       1
}

  • 1 Add an action to render my form for browser viewing.

In the next section, I use the passport package to start encrypting user data so that this login form will have a purpose.

25.3. Adding encryption with Passport.js

To start using Passport.js, I need to require the passport module in main.js and in users-Controller.js by adding const passport = require("passport") to the top of both files. These files are ones within which I’ll set up hashing and authentication. Next, I need to initialize and use passport within Express.js as middleware. Because passport uses sessions and cookies, I also need to require express-session and cookie-parser to main.js, adding the lines in listing 25.4 to that file.

To start using passport, I need to configure cookieParser with a secret key to encrypt the cookies stored on the client. Then I’ll have Express.js use sessions as well. This stage in the setup process is where passport starts to store information about active users of the application. passport officially becomes middleware by telling Express.js to initialize and use it on this line. Because sessions were set up before this line, I instruct Express.js to have passport use those preexisting sessions for its user data storage.

I set up the default login strategy, provided through the passport-local-mongoose module that I’ll soon add to the User model, to enable authentication for users with passport. The last two lines allow passport to compact, encrypt, and decrypt user data as it’s sent between the server and client.

Listing 25.4. Adding passport with Express.js in main.js
const passport = require("passport"),
  cookieParser = require("cookie-parser"),
  expressSession = require("express-session"),
  User = require("./models/user");

router.use(cookieParser("secretCuisine123"));             1
router.use(expressSession({
  secret: "secretCuisine123",
  cookie: {
    maxAge: 4000000
  },
  resave: false,
  saveUninitialized: false
}));                                                      2
router.use(passport.initialize());                        3
router.use(passport.session());                           4
passport.use(User.createStrategy());                      5
passport.serializeUser(User.serializeUser());             6
passport.deserializeUser(User.deserializeUser());

  • 1 Configure cookieParser with a secret key.
  • 2 Configure Express.js to use sessions.
  • 3 Configure Express.js to initialize and use passport.
  • 4 Instruct passport to use sessions.
  • 5 Set up the default login strategy.
  • 6 Set up passport to compact, encrypt, and decrypt user data.
Note

I need to make sure that the User model is required in main.js before I can use the createStrategy method. This method works only after I set up the User model with passport-local-mongoose.

With this configuration set up, I can move to the User model in user.js to add passport-local-mongoose. I need to require passport-local-mongoose in my User model by adding const passportLocalMongoose = require("passport-local-mongoose") to the top of user.js.

In this file, I attach the module as a plugin to userSchema, as shown in listing 25.5. This line sets up passportLocalMongoose to create salt and hash fields for the User model in my database. It also treats the email attribute as a valid field for logging in an authenticating. This code should be placed just above the module.exports line.

Listing 25.5. Adding passport-local-mongoose as a plugin to the User model
userSchema.plugin(passportLocalMongoose, {
  usernameField: "email"
});                          1

  • 1 Add the passport-local-mongoose module as a user schema plugin.
Note

With this addition to my User model, I no longer need the plain-text password property in the user schema. I’ll remove that property now, as well as the password table row on the user show page.

In the next section, I modify the create action in usersController.js to use passport for registering new users, and I set up flash messaging so that the user will know whether account creation is successful.

25.4. Adding flash messaging

With sessions and cookies ready to attach data to the request and respond to the user, I’m ready to integrate flash messaging by using connect-flash. To configure connect-flash, I need to require it in main.js as a constant, called connectFlash, by adding the following line: const connectFlash = require("connect-flash"). Then I tell my Express.js app to use it as middleware by adding router.use(connectFlash()) to main.js.

Now that the middleware is installed, I can call flash on any request in my application, which allows me to attach messages to the request. To get these request flash messages to my response, I add some custom middleware in main.js, as shown in listing 25.6. By telling the Express.js app to use this custom middleware, I’m able to assign a local variable called flashMessages to objects containing flash messages created in my controller actions. From here, I’ll be able to access the flashMessages object in my views.

Listing 25.6. Adding custom middleware to use flash messaging in main.js
router.use((req, res, next) => {
  res.locals.flashMessages = req.flash();        1
  next();
});

  • 1 Assign flash messages to a local variable.

Because I want flash messages to appear on every page, I’ll add some code to my layout .ejs file to look for flashMessages and display them if they exist. I’ll add the code in listing 25.7 to layout.ejs above the <%- body %>.

I intend to show only success and error messages. First, l check whether flashMessages is defined; then I display success messages or error messages that are attached to the object.

Listing 25.7. Adding logic to use flash messaging in layout.ejs
<div class="flashes">
<% if (flashMessages) { %>                                        1
  <% if (flashMessages.success) { %>
    <div class="flash success"><%= flashMessages.success %></div>
  <% } else if (flashMessages.error) { %>
    <div class="flash error"><%= flashMessages.error %></div>  <% } %>
<% } %>
</div>

  • 1 Display flash messages in the view.

Finally, I test this newly added code by modifying my user’s create action to use -passport and flash messaging by adding the code in listing 25.8 to usersController.js. The create action uses the register method provided by Passport.js to create a new user account. The result is a user document in my database with a hashed password and salt. If the user is saved successfully, I add a success flash message to be displayed in the index view. Otherwise, I show an error message on the user creation page.

Listing 25.8. Adding passport registration and flash messaging in the create action
create: (req, res, next) => {                                 1
  if (req.skip) next();

  let newUser = new User(getUserParams(req.body));

  User.register(newUser, req.body.password, (e, user) => {
    if (user) {
      req.flash("success", `${user.fullName}'s account
 created successfully!`);                                  2
        res.locals.redirect = "/users";
      next();
    } else {
      req.flash("error", `Failed to create user account
 because: ${e.message}.`);
      res.locals.redirect = "/users/new";
      next();
    }
  });
}

  • 1 Add the create action to register users.
  • 2 Respond with flash messages.

With this action in place, I’m ready to demo my new Passport.js registration process with flash messaging. Next, I add some custom validations before users are created.

25.5. Adding validation middleware with express-validator

The express-validator module provides useful methods for sanitizing and validating data as it enters this application. I start by requiring the module in main.js by adding const expressValidator = require( "express-validator") and telling my Express.js application to use this module as middleware by adding router.use(expressValidator()) to the same file.

I know that I want data to pass through some middleware validation function before it reaches the create action in the usersController, so I change my /users/create route to take that requirement into consideration, as shown in listing 25.9. This validate action lives in usersController and runs before the create action, which ensures that my custom validation middleware filters bad data before it gets a chance to reach my User model.

Listing 25.9. Adding a validation action before create in main.js
router.post("/users/create", usersController.validate,
 usersController.create, usersController.redirectView);     1

  • 1 Add validation middleware to the user create route.

Then I create the validate action in usersController.js by using the code in listing 25.10. This validate action parses incoming requests and cleans the data in the request body. In this case, I’m trimming whitespace from the first and last name fields.

I use some other methods provided by express-validator to keep the emails in my database consistent and the ZIP codes at the required length. I’ll also check to make sure that users entered some password when they signed up. I collect any errors that may have occurred during the validation steps. Then I concatenate the error messages into a single string. I set a property on the request object, req.skip = true, so that I skip the create action and go directly back to the view. All flash messages display in the users/new view. If there are no errors, I call next to move to the create action.

Listing 25.10. Adding a validate action in usersController.js
validate: (req, res, next) => {                                  1
  req
    .sanitizeBody("email")
    .normalizeEmail({
      all_lowercase: true
    })
    .trim();
  req.check("email", "Email is invalid").isEmail();
  req
    .check("zipCode", "Zip code is invalid")
    .notEmpty()
    .isInt()
    .isLength({
      min: 5,
      max: 5
    })
    .equals(req.body.zipCode);                                   2
 req.check("password", "Password cannot be empty").notEmpty();
 req.getValidationResult().then((error) => {
   if (!error.isEmpty()) {
     let messages = error.array().map(e => e.msg);
     req.skip = true;
     req.flash("error", messages.join(" and "));
     res.locals.redirect = '/users/new';                         3
     next();
   } else {
     next();
   }
 });
}

  • 1 Add the validate action.
  • 2 Sanitize and check input field data.
  • 3 Collect errors, and respond with flash messages.

The application is ready to validate data for user creation. The last step is connecting my login form to an authentication action I set up earlier.

25.6. Adding authentication with Passport.js

Passport.js makes my life easier by providing some default methods to use as middleware on requests. When I added passport-local-mongoose, my User model inherited even more useful methods than passport offered alone. Because the passport-local-mongoose module was added as a plugin on the User model, a lot of the authentication setup was taken care of behind the scenes.

The register method is one of the most powerful and intuitive methods provided by passport. To use it, I need to call passport.register and pass the login strategy that I plan to use. Because I’m using the default local strategy, I can create my authenticate action in usersController.js to use the passport.authenticate method as shown in listing 25.11.

Note

I need to make sure that const passport = require("passport") is at the top of my users controller.

This action points directly to the passport.register method. I’ve already created a local strategy for my User model in main.js and told passport to serialize and deserialize user data upon successful authentication. The options I add here determine which path to take if authentication succeeds or fails, with flash messages to go along.

Listing 25.11. Adding an authenticate action in usersController.js
authenticate: passport.authenticate("local", {        1
  failureRedirect: "/users/login",
  failureFlash: "Failed to login.",
  successRedirect: "/",
  successFlash: "Logged in!"
})

  • 1 Add authentication middleware with redirect and flash-message options.

I’m ready to test authentication with my login form at /users/login. Everything should be working at this point to log an existing user into the application. I need only to put some finishing touches on my layout file and add a logout link.

25.7. Logging in and out

I’ve already gotten the login process working. Now I’d like to add some visual indication that a user is logged in. First, I set up some variables that help me know whether there’s an unexpired session for a logged-in user. To do so, I add the code in listing 25.12 to my custom middleware, where I added the flashMessages local variable, in main.js.

With this middleware function, I have access to loggedIn to determine whether an account is logged in via the client from which the request was sent. isAuthenticated tells me whether there’s an active session for a user. currentUser is set to the user who’s logged in if that user exists.

Listing 25.12. Adding local variables to the response through middleware
res.locals.loggedIn = req.isAuthenticated();         1
res.locals.currentUser = req.user;                   2

  • 1 Set up the loggedIn variable to reflect passport login status.
  • 2 Set up the currentUser variable to reflect a logged-in user.

Now I can use these variables by adding the code in listing 25.13 to the navigation bar in my layout. I check to see whether loggedIn is true, telling me that a user is logged in. If so, I display the fullName of the currentUser linked to that user’s show page and a logout link. Otherwise, I display a sign-in link.

Listing 25.13. Adding a login status to my navigation bar in layout.ejs
<div class="login">
  <% if (loggedIn) { %>                            1
    <p>Logged in as
      <a href="<%=`/users/${currentUser._id}`%>">
  <%= currentUser.fullName %></a>
      <a href="/users/logout">Log out</a>
    </p>                                           2
  <%} else {%>
    <a href="/users/login">Log In</a>
  <% } %>
</div>

  • 1 Check whether a user is logged in.
  • 2 Display the current user’s name and logout link.

Finally, with my /users/logout route already in place, I need to add the logout action to my usersController, as shown in listing 25.14. This action uses the logout method on the incoming request. This method, provided by passport, clears the active user’s session. When I redirect to the home page, no currentUser exists, and the existing user is successfully logged out. Then I call the next middleware function to display the home page.

Listing 25.14. Adding a logout action to usersController.js
logout: (req, res, next) => {
  req.logout();                                        1
  req.flash("success", "You have been logged out!");
  res.locals.redirect = "/";
  next();
}

  • 1 Add an action to log users out.

With this last piece working, I can tell my contacts at Confetti Cuisine to advertise user accounts. When they log in successfully, the screen will look like figure 25.1. I’m confident that the registration and login process is safer, more reliable, and more intuitive than it was before.

Figure 25.1. Successful login on Confetti Cuisine

Summary

In this capstone exercise, I improved the Confetti Cuisine application by adding a few packages to make incoming data secure and more transparent to the user. With sessions and cookies installed, I’m able to use packages like passport and connect-flash to share information between the server and client about a user’s interaction with the Confetti Cuisine application. I added encryption to user passwords and two new user attributes set up by the passport-local-mongoose plugin on the User model. With stricter validations, my custom validate action serves as middleware to filter unwanted data and make sure form data meets my schema requirements. Last, with authentication in place, passport offers a way to track which users are logged in to my application, allowing me to cater specific content to registered users who are actively involved. In the next unit, I’ll add a few features to search content within the application, and in doing so, build an API on the server.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.239.123