Node.js GitHub Authentication using Passport.js and MongoDB
Joshua Evuetapha
Posted on November 8, 2021
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.
Setting up our Project
To start, create a Nodejs project by running this command.
npm init
Install the following packages by running these commands.
npm install express express-session ejs mongoose passport passport-github2 dotenv nodemon
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');
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' });
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');
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!'));
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}...`);
});
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!');
});
});
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
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');
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();
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');
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
}));
Here we redirect the user to the localhost:8081/auth
route once they visit localhost:8081/
app.get('/', function(req, res){
res.redirect('/auth');
});
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);
export the express app
module.exports = app;
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');
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');
We configure passport to use the github
middleware.
passport.use(github);
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());
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);
});
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 });
});
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'));
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;
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);
});
This route handler, handles get requests sent to this route localhost:8081/account
.
router.get('/', accountController.user);
module.exports = router;
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);
});
}
);
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;
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('/');
};
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});
};
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>
<% } %>
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>
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.
Posted on November 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.