After this lesson, you will be able to:
- Understand what Passport is and how it's used in web applications.
- Configure Passport as a middleware in our application.
- Allow users to log in to our application using Passport Local Mongoose.
- Create protected routes.
- Manage errors during the login process using the
connect-flash
package. - Allow users to logout from our application using Passport.
Passport is a flexible and modular authentication middleware for Node.js. Remember that authentication is the process where a user logs in a website by indicating their username and password.
If we can use username/email and password to log in a website, why should we use Passport? Passport also gives us a set of support strategies that for authentication using Facebook, Twitter, and more.
:::info First, we have to execute the following commands:
$ irongenerate nameOfYourProject
$ cd nameOfYourProject
$ npm i passport passport-local-mongoose express-session connect-flash
Finally, we should run the npm run dev
command:
$ npm run dev
Passport recognizes that each application has unique authentication requirements. Authentication mechanisms, known as strategies, are packaged as individual modules. Applications can choose which strategies to employ, without creating unnecessary dependencies.
As we said, Passport has a lot of authentication mechanisms called Strategies. Strategies are the process of being authenticated in many different ways. We can get authenticated in many ways:
- Local Strategies:
- Email & Password
- Username & Password
- Social Strategies:
First we'll learn the local strategy. This strategy could be a little bit tought to configure it, that's why we are going to use Passport Local Mongoose, this library is a Mongoose plugin that simplifies building local strategies. If you want to learn the thoughest way of how to do this, don't worry, we have a learning for you in the self guided lessons section.
We said Passport is a modular authentication middleware. So how do we build this authentication functionality into our application? Starting with an app generated with ironhack_generator
, we will create users with username and password, and authentication functionality using passport.
Create the model User.js
inside the models
folder. In User.js
, we will define the Schema with username and password as follows:
// models/User.js
const { model, Schema } = require("mongoose");
const userSchema = new Schema(
{
email: String,
name: String
},
{
timestamps: true
}
);
const User = model("User", userSchema);
module.exports = User;
Maybe you are thinking why we don't have a field password
, don't worry, Passport Local Mongoose will take care of the password and the hashing process 😉.
We have to modify a lit bit the User
model, adding Passport Local Mongoose to it.
// models/User.js
const { model, Schema } = require("mongoose");
const passportLocalMongoose = require('passport-local-mongoose');
const userSchema = new Schema(
{
email: String,
name: String
},
{
timestamps: true
}
);
const User = model("User", userSchema);
// We add the passport local mongoose super powers, we also define which field passport local mongoose will use
User.plugin(passportLocalMongoose, { usernameField: "email" });
module.exports = User;
The routes file will be defined in the routes/auth-routes.js
, and we will set the necessary packages and code to signup in the application:
// routes/auth-routes.js
const express = require("express");
const router = express.Router();
// User model
const User = require("../models/User");
router.get("/signup", (req, res, next) => {
res.render("auth/signup");
});
router.post("/signup", (req, res, next) => {
const { name, email, password } = req.body;
if (email === "" || password === "") {
return res.render("auth/signup", { message: "Indicate an email and password" });
}
User.findOne({ email })
.then(user => {
if (user !== null) {
return res.render("auth/signup", { message: "The username already exists" });
}
})
.catch(error => {
next(error);
});
User.register({ email, name }, password)
.then(userCreated => {
console.log(userCreated);
res.redirect("/login");
})
.catch(error => {
next(error);
})
});
module.exports = router;
We also need a form to allow our users to signup in the application. We will put the hbs
file in the following path: views/auth/signup.hbs
path. Create the /views/auth/
folder and place the signup.hbs
file inside it. The form will look like this:
<!-- views/auth/signup.hbs -->
<h2>Signup</h2>
<form action="/signup" method="POST" id="form-container">
<label for="name">Email</label>
<input id="name" type="text" name="name" placeholder="Kanye" />
<br /><br />
<label for="email">Email</label>
<input id="email" type="email" name="email" placeholder="[email protected]" />
<br /><br />
<label for="password">Password</label>
<input id="password" type="password" name="password" placeholder="Your password" />
{{#if message}}
<div class="error-message">{{ message }}</div>
{{/if}}
<br><br>
<button>Create account</button>
<p class="account-message">
Do you already have an account?
<a href="/login">Login</a>
</p>
</form>
Last, but not least, we will have to define the routes in the app.js
file. We will mount our authentication routes at the /
path.
// app.js
.
.
.
// routes
const authRouter = require("./routes/auth-routes");
app.use('/', authRouter);
We have created the user model to access the website through email and password. Now we are going to use Passport to log in our app. The first thing we have to do is to choose the Strategy we are going to use. A strategy defines how we will authenticate the user.
Before we start coding, we have to configure Passport in our app.
Let's create a folder called config
, and inside let's create the passport.js
config file.
// config/passport
const passport = require('passport');
const User = require('../models/user');
// We create the local strategy
passport.use(User.createStrategy());
// We serialize and deserialize the User
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
Passport works as a middleware in our application, so we should know how to add the basic configuration to it. If you want to know more about what is serialize and deserialize you can see this diagram.
Once our config file is ready, we have to require it in the app.js
file:
// app.js
const session = require("express-session");
// IMPORTANT! We require the config file that we created.
const passport = require("./config/passport");
Next up, we have to configure the middleware. First of all we have to configure the express-session
, indicating which is the secret key it will use to be generated:
// app.js
app.use(session({
secret: "our-passport-local-strategy-app",
resave: false,
saveUninitialized: true
}));
Then, we have to initialize passport and passport session, both of them like a middleware:
// app.js
app.use(passport.initialize());
app.use(passport.session());
This is all the middleware configuration we need to add to our application to be able to use Passport. The next step is to configure passport to support logging in.
First, we have to require the package we need to use passport in our routes. We will add this line at the beginning of the file:
const passport = require("passport");
Then, we have to define the routes and the corresponding functionality associated with each one. The GET
has no secret, we have to load the view we will use, meanwhile the POST
will contain the Passport functionality. The routes is in /routes/auth-routes.js
, and we have to add the following:
// routes/auth-route.js
router.get("/login", (req, res, next) => {
res.render("auth/login");
});
router.post("/login", passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login",
failureFlash: true,
}));
Cool, huh? We don't have to do anything else to be able to start a session with Passport! We need just 5 lines of code. Let's create the form to be able to log in.
Following the same file pattern we have used until now, we will create the form view in the /views/auth/login.hbs
path. It will contain the form, with username and password fields:
<form action="/login" method="POST">
<div>
<label for="email">Email:</label>
<input type="email" name="email">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password">
</div>
<div>
<input type="submit" value="Log In">
</div>
</form>
If we start the server, we will be able to log in. How can we prove we are logged in? Let's create a protected route to be 100% sure what we have done is working fine.
We are going to use create a middleware for ensure the login, in routes/auth-routes.js
// routes/auth-routes.js
function ensureLogin(req, res, next) {
if (req.isAuthenticated()) {
return next();
} else {
return res.redirect('/login');
}
}
Once it's written, we can add the following route below the rest of the routes:
router.get("/private-page", ensureLogin, (req, res, next) => {
res.render("private", { user: req.user });
});
As you can see, we are rendering a page that we should define in the /views/private.hbs
path. This page will just contain the following:
Private page
{{#if user}}
<div>
<a href="/">Index</a>
</div>
{{/if}}
Once you are logged in, you should be able to access the page.
We are almost done with Passport basic authentication. What happens when you log in and the credentials are wrong? The application crashes. Let's solve that by managing the errors.
The package connect-flash
is used to manage flash errors in Passport.
First we have to install the package in our project:
Once it's installed, we have to require it at the beginning of the app.js
:
const flash = require("connect-flash");
Once we've defined the package at the top of the file, we can change the LocalStrategy
configuration to use the flash messages as it follows:
app.use(flash());
In the router
, we have defined the route as follows:
router.get("/login", (req, res, next) => {
res.render("auth/login");
});
router.post("/login", passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login",
failureFlash: true,
}));
In line 28, we set a property called failureFlash
to true. This is what will allow us to use flash messages in our application. We just have to redefine the GET
method to send the errors to our view:
router.get("/login", (req, res, next) => {
res.render("auth/login", { "message": req.flash("error") });
});
Now we can add the following to/views/auth/login.hbs
to view the message:
<form action="/login" method="POST">
<div>
<label for="email">Email:</label>
<input type="email" name="email">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password">
</div>
{{#if message}}
<div class="error-message">{{ message }}</div>
{{/if}}
<div>
<input type="submit" value="Log In">
</div>
</form>
Once we have added the errors control, the login process is completed. To complete the basic authorization process, we have to create the logout method.
Passport exposes a logout()
function on req
object that can be called from any route handler which needs to terminate a login session. We will declare the logout
route in the auth-routes.js
file as it follows:
router.get("/logout", (req, res) => {
req.logout();
res.redirect("/login");
});
To finish up with this section, we just have to add a link requesting /logout
route in the browser, so we allow users to log out from our application.
In this learning unit we have seen that Passport is used to authenticate users in our application, but not for authorization.
We have reviewed how we can authorize users in our application, and how to combine this functionality with passport authentication.
We have also seen how we can protect routes and handle errors during the login process with different npm packages we have to install and configure.
Finally, we created the functionality to allow users log out from our application.