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:
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:
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.
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.
<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>
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.
I add all routing-specific code on the router object.
router.get("/users/login", usersController.login); 1 router.post("/users/login", usersController.authenticate); 2 router.get("/users/logout", usersController.logout, usersController.redirectView ); 3
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.
login: (req, res) => { res.render("users/login"); 1 }
In the next section, I use the passport package to start encrypting user data so that this login form will have a purpose.
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.
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());
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.
userSchema.plugin(passportLocalMongoose, { usernameField: "email" }); 1
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.
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.
router.use((req, res, next) => { res.locals.flashMessages = req.flash(); 1 next(); });
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.
<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>
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.
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(); } }); }
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.
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.
router.post("/users/create", usersController.validate, usersController.create, usersController.redirectView); 1
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.
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(); } }); }
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.
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.
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.
authenticate: passport.authenticate("local", { 1 failureRedirect: "/users/login", failureFlash: "Failed to login.", successRedirect: "/", successFlash: "Logged in!" })
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.
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.
res.locals.loggedIn = req.isAuthenticated(); 1 res.locals.currentUser = req.user; 2
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.
<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>
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.
logout: (req, res, next) => { req.logout(); 1 req.flash("success", "You have been logged out!"); res.locals.redirect = "/"; next(); }
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.
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.