DEV Community

Joshua Evuetapha
Joshua Evuetapha

Posted on

Node.js GitHub Authentication using Passport.js and MongoDB

In this article, you will learn how to authenticate with GitHub using Passport.js in a Nodejs express app.

You can get the source code for this project here, this project can be used as a boilerplate code when setting up an express app that uses Passportjs for authentication.

Prerequisites:

  • Basic Knowledge of NodeJS
  • Node JS should be installed on your system. ## What is Passport.js?

Passport is authentication middleware for Node.js. It is very flexible and modular. A comprehensive set of strategies supports authentication using a username and password, Google, Facebook, Apple, Twitter, and more. Find out more about Passport here.

Creating a Github Application

Before using passport-github2, you must have a Github account and register an application with Github. If you have not done this, You can do that here.
Your Homepage URI and Callback URI should match the one in your application. Your application will be issued a Client ID and Client secret, which this strategy needs to work.

Github Application setup page

Setting up our Project

To start, create a Nodejs project by running this command.

npm init
Enter fullscreen mode Exit fullscreen mode

Install the following packages by running these commands.

npm install express express-session ejs mongoose passport passport-github2 dotenv nodemon
Enter fullscreen mode Exit fullscreen mode

If the command is successful, you will see something like the image below, node_modules folder will be created and package-lock.json file will also be created.

Below is the project structure for this project.
📦passportjs
┣ 📂controller
┃ ┣ 📜account.js
┃ ┗ 📜auth.js
┣ 📂model
┃ ┗ 📜UserModel.js
┣ 📂routes
┃ ┣ 📜account.js
┃ ┗ 📜auth.js
┣ 📂utils
┃ ┗ 📜github.js
┣ 📂views
┃ ┣ 📜account.ejs
┃ ┗ 📜index.ejs
┣ 📜.env
┣ 📜.gitignore
┣ 📜app.js
┣ 📜package-lock.json
┣ 📜package.json
┣ 📜README.md
┗ 📜server.js

Setting up our Express Server

At this point, our application set. Now let’s go ahead and set up our express server. To get started, first create server*.js file* in the project root directory.
Next, import the mongoose for our database connection and dotenv to lead our environment variables with the code below:

const mongoose = require('mongoose');
const dotenv = require('dotenv');
Enter fullscreen mode Exit fullscreen mode

Next, create a .env file in your project root directory, where we will store our environment variables later in this session. Then make the available in our application with the code below.

dotenv.config({ path: './.env' });
Enter fullscreen mode Exit fullscreen mode

Import app.js into the code in this file export an express app, this app will be explained next in this article.

const app = require('./app');
Enter fullscreen mode Exit fullscreen mode

Next, we make a connection to the mongoose database with the code below.

mongoose
  .connect(process.env.DATABASE, { useUnifiedTopology: true })
  .then(() => console.log('DB connection successful!'));
Enter fullscreen mode Exit fullscreen mode

Next, we assign a port to the express application. The application will be listening to the port provided by the environment or port 8081 if there is no environment port.

const port = process.env.PORT || 8081;

app.listen(port, () => {
  console.log(`App running on port ${port}...`);
});
Enter fullscreen mode Exit fullscreen mode

The following line of codes listens for the following events uncaughtException, unhandledRejection, and SIGTERM respectively, and shut down the server once either one of them occurs.

process.on('uncaughtException', err => {
    console.log('UNCAUGHT EXCEPTION! 💥 Shutting down...');
    console.log(err.name, err.message);
    process.exit(1);
});

process.on('unhandledRejection', err => {
    console.log('UNHANDLED REJECTION! 💥 Shutting down...');
    console.log(err.name, err.message);
    server.close(() => {
      process.exit(1);
    });
});

process.on('SIGTERM', () => {
    console.log('👋 SIGTERM RECEIVED. Shutting down gracefully');
    server.close(() => {
      console.log('💥 Process terminated!');
    });
});
Enter fullscreen mode Exit fullscreen mode

Your .env file should look like this. Put your credentials on the required fields.

DATABASE = your-mongo-db-uri
GITHUB_CLIENT_ID = your-github-app-client-id
GITHUB_CLIENT_SECRET = your-github-app-client-secret
GITHUB_CALLBACK_URL = your-github-app-callback-url
SESSION_SECRET = your-app-session-secret-it-can-be-any-string-of-your-choice
Enter fullscreen mode Exit fullscreen mode

Setting up our Express Application

Now let’s go ahead and setup our express application. To get started, create an app.js in the project root directory. first we import express, next we import express-session this is a middleware for handling user sessions in express.
Then import two route handlers on for handling authentication request and the other for handling request in user account. These route handlers will be explained next.

const express = require('express');
const session = require('express-session');
const authRouter = require('./routes/auth');
const accountRouter = require('./routes/account');
Enter fullscreen mode Exit fullscreen mode

Here is where we create the express and by calling the express function which is a top level function exported by the express module and assign it to the app variable.

const app = express();
Enter fullscreen mode Exit fullscreen mode

Next, we configure the directory where the template files will be located. The first line of code set the view directory to /views. The second line set the view engine to ejs. Learn more about ejs here.

app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
Enter fullscreen mode Exit fullscreen mode

Next we use the express-session middleware so that we can support persistent login from users. the session(options) receives an object of settings read the express-session documentation to learn more.

The default server-side session storage, MemoryStore, is not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing. For a list of stores, see compatible session stores.

app.use(
  session(
    { 
      secret: process.env.SESSION_SECRET, 
      resave: false, 
      saveUninitialized: false 
    }));
Enter fullscreen mode Exit fullscreen mode

Here we redirect the user to the localhost:8081/auth route once they visit localhost:8081/

app.get('/', function(req, res){
  res.redirect('/auth');
});
Enter fullscreen mode Exit fullscreen mode

Next, we configure two routers on the app for handling localhost:8081/auth/* requests and the other for handling account request localhost:8081/account/* these routers will be discussed next.

// set Routes
app.use('/auth', authRouter);
app.use('/account', accountRouter);
Enter fullscreen mode Exit fullscreen mode

export the express app

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

Creating our Application Routers

First, we create a route directory. The files in this directory will be used as route handlers to handle different routes in our application.

Create Authentication Router
Create auth.js file inside the route directory, then import express and passport.

const express = require('express');
const passport = require('passport');
Enter fullscreen mode Exit fullscreen mode

We import github which is an authentication middleware based on passport GitHub strategy this middleware will be explained later in this article. Also, import authController. This module is meant to contain a bunch of functions that control user authentication but for now, it just contains the logout function.

const github = require('./../utils/github');
const authController = require('./../controller/auth');
Enter fullscreen mode Exit fullscreen mode

We configure passport to use the github middleware.

passport.use(github);
Enter fullscreen mode Exit fullscreen mode

Here we use the express.Router() class to create modular, mountable route handlers. then we use the passport.initialize() function in the router this function is needed to initialize passportjs on our routes, passport.session() function enables persistent login with passportjs in our route it handles session.

const router = express.Router();

router.use(passport.initialize());
router.use(passport.session());
Enter fullscreen mode Exit fullscreen mode

serializeUser determines which data of the user object should be stored in the session. The result of the serializeUser function is attached to the session as req.session.passport.user = {}. Here we store the whole user object

The first argument of deserializeUser corresponds to the user object that was given to the done function. The object is attached to the request object as req.user

passport.serializeUser(function(user, done) {
    done(null, user);
});

passport.deserializeUser(function(obj, done) {
    done(null, obj);
});
Enter fullscreen mode Exit fullscreen mode

This line of code renders the index.ejs file in the view directory once the user visits the localhost:8081/auth route.

router.get('/', function(req, res){
    res.render('index', { user: req.user });
});
Enter fullscreen mode Exit fullscreen mode

This line of codes tries to authenticate the user with GitHub once the
localhost:8081/auth/github route is visited. It Redirect the user to a GitHub conscent page and request for the user authorization, once the user authorize the app, it redirects the user back to the callback url which is localhost:8081/auth/github/callback for this application on successful login the user will be redirected to localhost:8081/account by this line of code res.redirect('/account')); .

router.get('/github', passport.authenticate('github', { scope: [ 'user:email' ] }));

router.get('/github/callback', 
    passport.authenticate('github', { failureRedirect: '/' }),
    (req, res) =>  res.redirect('/account'));
Enter fullscreen mode Exit fullscreen mode

Once the user visit localhost:8081/auth/logout. the session will be destroyed and the user will have to login again.

router.get('/logout', authController.logout);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Create account routes
Create account.js file inside the route directory, the following codes below do the same function as the ones on auth.js , accountController.js contains functions for handling user accounts.

const express = require('express');
const passport = require('passport');
const accountController = require('./../controller/account');

const router = express.Router();

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

passport.serializeUser(function(user, done) {
    done(null, user);
});

passport.deserializeUser(function(obj, done) {
    done(null, obj);
});
Enter fullscreen mode Exit fullscreen mode

This route handler, handles get requests sent to this route localhost:8081/account.

router.get('/', accountController.user);
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Creating Utility

First, we create a utils directory. This directory is going to contain all our utility functions, for this project.

Create github middleware
This code exports a middleware this middleware is required when making an authentication request with passport-github2 strategy. Here we use passport-github2 strategy, we pass the configuration object which includes the ClientId, ClientSecret, and CallbackUrl, these values should match the one used in creating the github application. if these values are correct and up to date the callback function with four parameters with be called

  • accessToken - GitHub access Token
  • refreshToken - GitHub refresh Token
  • profile - contains user data gotten from GitHub
  • done - this is callback function with two arguments error and data is called, the profile.id data is used to query the mongo database to check if the user account exists, if it doesn't exist the user is created with the data gotten from github.

User.findOne({githubId: profile.id }) checks if a user with the same github profile exists in the database, if it does exist the return done(null, data); function will be called with the user data. If no user exists the user will be created and the return done(null, data); will be called with the user data.

const GitHubStrategy = require('passport-github2').Strategy;
const User = require('../model/UserModel');

module.exports = new GitHubStrategy({
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: process.env.GITHUB_CALLBACK_URL
  },
  function(accessToken, refreshToken, profile, done) {

    User.findOne({githubId: profile.id }).then((data, err) => {

      if (!data) return User.create({
        githubId: profile.id,
        fullname: profile.displayName,
        username: profile.username,
        location: profile._json.location,
        phone: profile._json.phone,
        email: profile._json.email,
        profilePhoto: profile._json.avatar_url
      }).then((data, err) => {
        return done(null, data);
      });

      else return done(null, data);
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

Creating Database Model

Create a model directory. This directory is going to contain all our database Models, for this project.

Create User Model
First, we create a userModel.js file inside the model directory, import mongoose into the project, then create a user schema.

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

Models are fancy constructors compiled from Schema definitions. An instance of a model is called a document. Models are responsible for creating and reading documents from the underlying MongoDB database.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema(
  {
    fullname: { type: String },
    username: { type: String },
    githubId: { type: String, unique: true },
    location: { type: String },
    phone: { type: String },
    email: { type: String, lowercase: true },
    profilePhoto: { type: String, default: '' }
  },
  { timestamps: true }
);

const User = mongoose.model('User', userSchema);

module.exports = User;
Enter fullscreen mode Exit fullscreen mode

Creating Controllers

Create a controller directory. This directory is going to contain all our controllers for this project.

Authentication controller
The auth.js controller contains one function logout to destroy user session and redirect user to the homepage.

exports.logout = (req, res, next) => {
    req.logout();
    res.redirect('/');
};
Enter fullscreen mode Exit fullscreen mode

Authentication controller
The account.js controller contains one function user, req.user get the user data from the request parameter, if (!user) res.redirect('/'); redirect the user to localhost:8081/ if the user exist it readers the account.ejs templete.

exports.user = (req, res, next) => {
    const user = req.user;

    if (!user) res.redirect('/');

    res.render('account', {user: user});
};
Enter fullscreen mode Exit fullscreen mode

Creating Views

Create a views directory, this directory will hold all the ejs templating codes for the application.

Create the Index ejs template
Create a file index.ejs inside the views directory. This templete renders a link to authenticate with github when user session is not available <h2>Welcome! <a href="/auth/github">Login with GitHub</a> </h2> and renders a link to view user account, when user session is available <h2>Hello, <%= user.fullname %> <a href="/account">View Account</a></h2>.

<% if (!user) { %>
    <h2>Welcome! <a href="/auth/github">Login with GitHub</a> </h2>
<% } else { %>
    <h2>Hello, <%= user.fullname %> <a href="/account">View Account</a></h2>
<% } %>
Enter fullscreen mode Exit fullscreen mode

Create the Account ejs template
Create a file account.ejs inside the views directory. This template simply displays user information, stored in the database.

<div>
<p>Full Name: <%= user.fullname %></p>
<p>Username: <%= user.username %></p>
<p>Email: <%= user.email %></p>
<p>location: <%= user.location %></p>
<p><a href="/auth/logout">Logout </a></p>
<img src=<%= user.profilePhoto %> />
</div>
Enter fullscreen mode Exit fullscreen mode




Conclusion

In this article you have learned how to authenticate users, using passport-github2 strategy, You learned how to create and configure a github application, and maintain user sessions within your application while using ejs as templating engine.

Latest comments (0)