Authentication System using Passport.js, Node.js, and MongoDB - Part 1: Google Login

In this guide, we will create an authentication system that will let our users log in using Google Sign-In.

Authentication System using Passport.js, Node.js, and MongoDB - Part 1: Google Login
  • Languages: Node, Express.js
  • Tools Used: Passport.js
  • Time Saved: 4 weeks -> 30 mins
  • Source Code
  • Live Demo

This is Part 1 in the series of guides on creating an authentication system using Passport.js, Node.js and Mongo for your SaaS app.

Introduction

Authentication is critical in verifying a user's identity. Passport.js makes it easy to set up OAuth2 login strategies from all identity providers.

Basic login screen that allows signing in with a Google account

In Part 1, we will:

  1. Build a Google Login button to authenticate users
  2. Save and manage user data in MongoDB
  3. Give useful notifications using Express Flash to the user

Setup the Google Cloud Project

In order to add the Sign in with Google button, we have to create the Google Cloud project which will give us the relevant client key and secret to be able to communicate with Google to authenticate a user.

image-20210605160540783
Google Cloud Console Dashboard
  1. Navigate to Google Cloud Console and add a New Project.
  2. On the Dashboard, click on Go to APIs overview
  3. Select the Credentials tab in the panel on the left side
  4. Click Create Credentials and then OAuth Client ID
  5. Select Application Type to be Web
  6. Add Authorized Redirect URIs to be: http://localhost:3000/auth/google and  http://localhost:3000/auth/google/callback
  7. Google will generate unique Client ID and Client Secret keys for project. Create a new file called .env and add your keys and callback URL in there like so,
CLIENT_ID=107xxx-ni31ps789mf1jd33nnfk57vdllhqcmie.apps.googleusercontent.com
CLIENT_SECRET=KE5xxxvvm
CALLBACK_URL=http://localhost:3000/auth/google/callback

Our Google Project is all setup and ready to accept requests from our application.

Setup the Node.js project

Let's start by creating a new project folder. Add a package.json

{
  "name": "auth-basic-nodejs",
  "version": "1.0.0",
  "author": "sssaini",
  "description": "auth-basic",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "body-parser": "^1.18.2",
    "connect-flash": "^0.1.1",
    "cookie-parser": "^1.4.5",
    "dotenv": "^8.2.0",
    "ejs": "^3.1.5",
    "express": "^4.17.1",
    "express-flash": "0.0.2",
    "express-session": "^1.17.1",
    "mongoose": "^5.7.1",
    "passport": "^0.4.1",
    "passport-google-oauth20": "^2.0.0",
    "passport-local": "^1.0.0",
    "uuid": "^8.3.2"
  }
}

Install dependencies by running,

npm install

Set up a basic Express Server

Create a new file named app.js

require("dotenv").config();

const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.engine("html", require("ejs").renderFile);
app.use(express.static(__dirname + "/public"));

app.use(cookieParser());

app.get("/", (req, res) => {
  res.render("index.ejs");
});

app.listen(3000, function () {
  console.log("SaaSBase Authentication Server listening on port 3000");
});

We will use the public folder to serve any static files like images, CSS, and JS. We are also using a templating engine called EJS so we can generate a dynamic HTML page by rendering information sent by the server.

Let's add a simple view in the file, views/index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
  </head>
  <body>
    <p>Hello World</p>
  </body>
</html>

Launch the app by running,

npm start

The application should be live at http://localhost:3000.

All the basic setup is now done! Let's move on to the meat of the application.

Start a Local Mongo DB

We need a database. MongoDB is a very good choice. Download the community version here. Run,

mongod

A local Mongo instance should start up on mongo://localhost:27017

Add MongoDB Connection to App

Add the MongoDB connection to app.js:

const mongoose = require('mongoose')

const db = 'mongodb://localhost:27017/auth'
mongoose.connect(
  db,
  {
    useUnifiedTopology: true,
    useNewUrlParser: true,
    useFindAndModify: false,
    useCreateIndex: true
  },
  (error) => {
    if (error) console.log(error)
  }
)

Create the User Model

Create a new file called, src/user/user.model.js

let mongoose = require("mongoose");
let Schema = mongoose.Schema;

const userSchema = new Schema({
  id: {
    type: String,
    default: null,
  },
  email: {
    type: String,
    required: [true, "email required"],
    unique: [true, "email already registered"],
  },
  firstName: String,
  lastName: String,
  profilePhoto: String,
  password: String,
  source: { type: String, required: [true, "source not specified"] },
  lastVisited: { type: Date, default: new Date() }
});

var userModel = mongoose.model("user", userSchema, "user");

module.exports = userModel;

Create a new file called - src/user/user.service.js

const addGoogleUser = (User) => ({ id, email, firstName, lastName, profilePhoto }) => {
  const user = new User({
    id, email, firstName, lastName, profilePhoto, source: "google"
  })
  return user.save()
}

const getUsers = (User) => () => {
  return User.find({})
}

const getUserByEmail = (User) => async ({ email }) => {
  return await User.findOne({ email })
}

module.exports = (User) => {
  return {
    addGoogleUser: addGoogleUser(User),
    getUsers: getUsers(User),
    getUserByEmail: getUserByEmail(User)
  }
}

We can now inject the model i nto the service by adding src/user/index.js

const User = require('./user.model')
const UserService = require('./user.service')

module.exports = UserService(User)

Set up Passport.js for Google

Passport.js is a great NPM package that simplifies adding a social login to a web app. We will use the Google Strategy to authenticate a user with their Gmail account.

Create a file called src/config/google.js:

const passport = require("passport");
const User = require("../user/user.model");
const GoogleStrategy = require("passport-google-oauth20").Strategy;

passport.use(
  new GoogleStrategy(
    {
      callbackURL: process.env.CALLBACK_URL,
      clientID: process.env.CLIENT_ID,
      clientSecret: process.env.CLIENT_SECRET,
    },
    async (accessToken, refreshToken, profile, done) => {
     console.log("user profile is: ", profile)
    }
  )
);

Perfect, let's now add the strategy as part of a GET route so we can invoke it and trigger the Google Auth flow.

In app.js:

//...

const passport = require("passport");

require("./src/config/google");

//...

app.use(passport.initialize());
app.use(passport.session());

app.get(
  "/auth/google",
  passport.authenticate("google", {
    scope: ["profile", "email"],
  })
);

app.get(
  "/auth/google/callback",
  passport.authenticate("google", {
    failureRedirect: "/",
    successRedirect: "/profile",
    failureFlash: true,
    successFlash: "Successfully logged in!",
  })
);

Run the application with:

npm start

Once started, we can try logging in by navigating to http://localhost:3000/auth/google

Google's OAuth Login screen triggered by "Sign in with Google" button

The application might hang after a successful login but don't worry about that just yet. We will fix that in a bit. Looking at the console however, we will see a JSON response from Google like so,

{
  id: 'xxx',
  displayName: 'Sukhpal Saini',
  name: { familyName: 'Saini', givenName: 'Sukhpal' },
  emails: [ { value: '[email protected]', verified: true } ],
  photos: [
    {
      value: 'https://lh3.googleusercontent.com/a-/AOh14GjSb0KZnfOv_wP8cT6M7wEoGMlpG1fFbsO1Uszm7lw=s96-c'
    }
  ],
  provider: 'google',
  _raw: '{\n' +
    '  "sub": "xxx",\n' +
    '  "name": "Sukhpal Saini",\n' +
    '  "given_name": "Sukhpal",\n' +
    '  "family_name": "Saini",\n' +
    '  "picture": "https://lh3.googleusercontent.com/a-/AOh14GjSb0KZnfOv_wP8cT6M7wEoGMlpG1fFbsO1Uszm7lw\\u003ds96-c",\n' +
    '  "email": "[email protected]",\n' +
    '  "email_verified": true,\n' +
    '  "locale": "en-GB"\n' +
    '}',
  _json: {
    sub: 'xxx',
    name: 'Sukhpal Saini',
    given_name: 'Sukhpal',
    family_name: 'Saini',
    picture: 'https://lh3.googleusercontent.com/a-/AOh14GjSb0KZnfOv_wP8cT6M7wEoGMlpG1fFbsO1Uszm7lw=s96-c',
    email: '[email protected]',
    email_verified: true,
    locale: 'en-GB'
  }
}

Perfect, Google has just validated our user for us and provided us with their profile information.

Save User Details

We can save this information in our MongoDB database.

In config/google.js

const passport = require("passport");
const UserService = require('../user')
const GoogleStrategy = require("passport-google-oauth20").Strategy;

passport.use(
  new GoogleStrategy(
    {
      callbackURL: process.env.CALLBACK_URL,
      clientID: process.env.CLIENT_ID,
      clientSecret: process.env.CLIENT_SECRET,
    },
    async (accessToken, refreshToken, profile, done) => {
      const id = profile.id;
      const email = profile.emails[0].value;
      const firstName = profile.name.givenName;
      const lastName = profile.name.familyName;
      const profilePhoto = profile.photos[0].value;
      const source = "google";


      const currentUser = await UserService.getUserByEmail({ email })

      if (!currentUser) {
        const newUser = await UserService.addGoogleUser({
          id,
          email,
          firstName,
          lastName,
          profilePhoto
        })
        return done(null, newUser);
      }

      if (currentUser.source != "google") {
        //return error
        return done(null, false, { message: `You have previously signed up with a different signin method` });
      }

      currentUser.lastVisited = new Date();
      return done(null, currentUser);
    }
  )
);

Create a file called config/passport.js. This will keep our passport configuration.

const passport = require("passport");
const User = require("../user/user.model");

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  const currentUser = await User.findOne({ id });
  done(null, currentUser);
});

We can add the passport configuration to app.js

//...

require("./src/config/passport");
require("./src/config/google");

//...

Build the UI - Google Login screen

Add a new file - views/index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="description" content="" />
    <title>SaaSBase - Authentication</title>

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
      crossorigin="anonymous"
    />

    <link href="./css/index.css" rel="stylesheet" />
  </head>
  <body>
    <p class="links">
      <a href="/local/signup">Sign up using Email →</a>
    </p>
    <div class="content">
      <form class="text-center" action="/auth/google" method="get">
        <a href="/"
          ><img
            class="mb-4"
            src="./images/saasbase.png"
            alt=""
            width="72"
            height="72"
        /></a>
        <h1 class="h3 mb-3 font-weight-normal">Sign in with Google</h1>
        <p class="mb-3 text-muted">
          Implement a Google OAuth strategy using Passport.js and MongoDB
        </p>

        <button class="btn btn-primary btn-block my-2" type="submit">
          Sign in with Google
        </button>
      </form>
      
      <div class="mt-5 mb-3 text-center">
          <a class="text-muted" href="https://saasbase.dev">
            Built by SaaSBase
          </a>
        </div>
    </div>
    <script
      src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

Add a new CSS file - public/css/index.css and copy over the styles from here.

Completed login screen with styling that allows signing in with a Google account

Add Flash Messages

With Passport.js we can communicate back to the user when their credentials are rejected using flash messages. To add them in, edit app.js

const flash = require("express-flash");
const session = require("express-session");

app.use(
  session({
    secret: "secr3t",
    resave: false,
    saveUninitialized: true,
  })
);

app.use(flash());

To render these messages in EJS, we can edit views/index.ejs

<button class="btn btn-primary btn-block my-2" type="submit">Sign in with Google</button>

<% if ( messages.error) { %>
	<p class="error-text"><%= messages.error %></p>
<% } %>
    
</form>
Login screen with flash messages that notifies the user when a sign in attempt fails

Build the UI - Profile screen

After a successful login, we should send the user to a profile screen where they can see their data.

The profile route has to be protected so that non logged in users cannot go to the page and are redirected.

In app.js

const isLoggedIn = (req, res, next) => {
  req.user ? next() : res.sendStatus(401);
};

app.get("/profile", isLoggedIn, (req, res) => {
  res.render("profile.ejs", { user: req.user });
});

Create a new file named src/views/profile.ejs

<!DOCTYPE html>
<html lang="en" class="h-100">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="description" content="" />
    <title>SaaSBase - Authentication</title>

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
      crossorigin="anonymous"
    />
  </head>
  <body class="d-flex flex-column h-100">
    <!-- Begin page content -->
    <main role="main" class="flex-shrink-0">
      <div class="container">
        <h1 class="mt-5">User Profile</h1>
        <hr />
        <% if ( user ) { %>
        <div class="row">
          <div class="col-md-2">
            <img
              src="https://lh3.googleusercontent.com/a-/AOh14GjSb0KZnfOv_wP8cT6M7wEoGMlpG1fFbsO1Uszm7lw=s96-c"
              class="img-fluid"
              style="width: 100%"
            />
          </div>
          <div class="col-md-10">
            <h3>
              <%= user.firstName %> <%= user.lastName %> (#<%= user.id %>)
            </h3>
            <p>Logged in using <%= user.source %></p>
            <p><%= user.email %></p>
            <p>Last visted on <%= user.lastVisited %></p>
            <p><a href="/auth/logout">Logout</a></p>
          </div>
        </div>
        <% } else { %>
        <h1>User not found</h1>
        <% } %>
      </div>
    </main>

    <footer class="footer mt-auto py-3">
      <div class="container">
        <span class="text-muted"
          >The full guide is available at
          <a href="https://saasbase.dev/">saasbase.dev</a></span
        >
      </div>
    </footer>
    <script
      src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

Logout a User

In app.js

app.get("/auth/logout", (req, res) => {
  req.flash("success", "Successfully logged out");
  req.session.destroy(function () {
    res.clearCookie("connect.sid");
    res.redirect("/");
  });
});

Next Steps

Congratulations, we just built a great looking Google Sign In button! We learned how to:

  1. Build a Google Login button to authenticate users
  2. Save and manage user data in MongoDB
  3. Give useful notifications using Express Flash to the user

There's still room for improvement. Currently, our application can only be used by folks that have a Google account. We can solve this by having a local signup as an authentication method.

Didn't find this guide useful? Let me know