@Version : 0.1.0
@Build : abcd
By using this site, you acknowledge that you have read and understand the Cookie Policy, Privacy Policy, and the Terms. Close


NextJS OAuth with Passport and Github

Posted Saturday, March 14th, 2020

NextJSReactJSNodeJS
NextJS OAuth with Passport and Github

Prerequisites

In this post we setup OAuth between a NextJS client and an Express API. I have assumed knowledge of JavaScript, Nodejs and Expressjs in this post. I however explain things well enough for anyone to catch up and understand things.

This post is composed of two parts.

  1. API - Involves the REST API backend part which has the OAuth collection of endpoints and delete profile endpoint. This will involve creating REST endpoints, a GitHub OAuth and a MongoDB database for storing user details.
  2. UI - This part includes setting a Nextjs Server Side Rendered application which has a single page for doing login.

OAuth Process.

So to make OAuth work take a look at the diagram below. oauth-with-github-process User clicks the login button which calls our API /auth/github. Our API uses GitHub passport library to redirect the user to their GitHub account to either approve or decline the authentication. After user approves the authentication, Passportjs will call back our auth callback endpoint /auth/github/callback with the user details. We then use the user details to generate a token that we redirect the User back to our UI with and complete the authentication process.

Lets roll šŸš€šŸš€šŸš€

Creating the API

Run npm init to create a new Nodejs app.

ā•­ā”€ā–‘ā–’ā–“ ~/Code/nextjs-passport/api | on master
ā•°ā”€ npm init 
{
  "name": "api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
ā•­ā”€ā–‘ā–’ā–“ ~/Code/nextjs-passport/api | on master
ā•°ā”€

Install packages express, body-parser, passport, passport-github, jsonwebtoken and mongoose that make up our API server.

npm i --save express passport passport-github mongoose jsonwebtoken

Our base server will look like this. Using body parser to parse request body and listen on port 3001 Create app.js

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// Parse json encoded in the request body
app.use(bodyParser.json({ limit: '50mb' }));

// start server
app.listen(3001, () => console.log("Server listening on http://localhost:3001"))

Lets allow cors so that our UI will be able to call the API without cross origin requests restrictions. This will be a middleware that mounts in the apps request chain.

....
// allow cors from all - no hustle and never safe
app.use((_, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', '*');
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    next();
})

// start server
app.listen(3001, () => console.log("Server listening on http://localhost:3001"))

The next thing we do at this point is to setup passport to serialize the user for downstream process. Passport will serialized user upon receiving the OAuth response then leave everything to us. We will handle session and auth ourselves using cookie after OAuth is negotiation is done.

const express = require('express');
const bodyParser = require('body-parser');
const passport = require('passport');

.........

app.use(passport.initialize());

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

// start server
app.listen(8090, () => console.log("Server listening on http://localhost:8090"))

So the above code will be used by the communication between passport and passport-github Now its time to setup OAuth part that will talk to Github API which will comprise the login endpoint. For this we will use a dedicated route /auth and some database utilities using Mongoose for storing our user details from the OAuth call.

Setup the Data layer

This comprises anything you like but I am using Mongoose to connect to a local mongodb. Mongoose is useful for setting up quick connections and models.

Create db/mongoose

const mongoose = require('mongoose');

mongoose.connect("mongodb://localhost:27017",
    {
        useNewUrlParser: true,
        useUnifiedTopology: true
    }
);

const db = mongoose.connection;

db.on('error', (e) => {
    logger.error(e.toString(), true);
    logger.error(e.stack, true);
    process.exit(999);
});

db.once('open', async function () {
    console.info('DB Connected Successfully');
});

module.exports = mongoose;

Start the db service connection when app start. Edit app.js and require db.

const express = require('express');
const bodyParser = require('body-parser');
require('./db/mongoose');

.........

Then we create the user model. Comprising just the oAuthId - The user id on GitHub and oAuthData - The rest of the data.

Create db/models/user

const mongoose =  require('../mongoose.js');

const Schema = mongoose.Schema;
const userSchema = new Schema({
  oAuthId: { type: Number, required: true },
  oAuthData: { type: Object, required: true}
});

module.exports = mongoose.models.User || mongoose.model('User', userSchema);

Next we create the DB utilities for creating and getting our user from Mongodb

db/services/user.js

const User = require('../models/user');

module.exports = {
  findOrCreate: async (oAuthData) => {
    try {
      const user = await User.findOne({ oAuthId: oAuthData.id });
      if (!user) {
        const newUser = new User({oAuthId: oAuthData.id, oAuthData: oAuthData});
        await newUser.save();
        return newUser;
      }
      return user;
    } catch (e) {
      return Error('User not found');
    }
  },
  fineById: async (id) => {
    return User.findOne({ oAuthId: id });
  }
};

Now we are ready to create our auth endpoints. But before that go to GitHub OAuth Apps settings and create a new app. Get the values Client ID and Client Secret and set to local environment as below

export GITHUB_CLIENT_ID=Client ID from GitHub
export  GITHUB_CLIENT_SECRET=Client Secret from GitHub

Now create the file below that holds the endpoints that login calls and intern calls GitHub APIs for us. Here we call GitHub and authenticate then generate a new token that we send back to the UI in the URL query

Create routes/auth.js

const express = require('express');
const jwt = require('jsonwebtoken');
const passport = require('passport');
const { Strategy } = require('passport-github');

const users = require('../db/services/user');
const JWT_KEY = "something_private_and_long_enough_to_secure"

const router = express();

passport.use(new Strategy({
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  callbackURL: "http://localhost:3001/auth/github/callback"
},

function (accessToken, refreshToken, profile, cb) {
  users.findOrCreate(profile);
  return cb(null, profile);
}
));

router.get('/github', (req, res, next) => {
  const { redirectTo } = req.query;
  const state = JSON.stringify({ redirectTo });
  const authenticator = passport.authenticate('github', { scope: [], state, session: true });
  authenticator(req, res, next);
}, (req, res, next) =>{
  next();
});

router.get(
  '/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }), (req, res, next) => {
    const token = jwt.sign({id: req.user.id}, JWT_KEY, {expiresIn: 60 * 60 * 24 * 1000})
    req.logIn(req.user, function(err) {
      if (err) return next(err); ;
      res.redirect(`http://localhost:3000?token=${token}`)
    });
        
  },
);
module.exports = router;

And the we mount the route to our app, edit app.js and add route.

......

app.use('/auth', require('./routes/auth'))

// start server
app.listen(3001, () => console.log("Server listening on http://localhost:3001")

Our API is now ready to start accepting OAuth requests

Creating the UI

Create a new Nextjs app using npx create-next-app and follow the instructions. After creating a nextjs app, we will edit the index page to have a Login button.

import Head from 'next/head'

const Home = () => (
  <div className="container">
    <Head>
      <title>Nextjs OAuth with GitHub</title>
      <link rel="icon" href="/favicon.ico" />
    </Head>

    <man>
      <h1>Welcome to Nextjs OAuth with GitHub</h1>
      <a href={"http://localhost:3001/auth/github"} >Click here to login</a>
    </man>
  </div>
)

export default Home

Now at this point clicking login should redirect number of times and lead us back to the index page with a cookie called authorization in the browsers cookie section. So the next thing we do is to find this cookie when serving our page using Nextje server.

To set the cookie, as the token is sent in the redirect URL after login, we use a custom Document in our Next app.

import Document, { Html, Head, Main, NextScript } from 'next/document'
import { setCookie } from 'nookies';

class MyDocument extends Document {
    static async getInitialProps(ctx) {
        const initialProps = await Document.getInitialProps(ctx)
        if (ctx.query.token) {
            setCookie(ctx, 'authorization', ctx.query.token, {
                maxAge: 30 * 24 * 60 * 60,
                path: '/',
            });
        }

        return { ...initialProps }
    }

    render() {
        return (
            <Html>
                <Head />
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        )
    }
}

export default MyDocument

For reading the cookie on the server side, we use Nookies to read the token while also considering it in the request query parameters.

npm install --save nookies

Then we add a getInitialProps to our index page component to read the cookie.

import Head from 'next/head'
import { parseCookies } from 'nookies'

const Home = (props) => (
  <div className="container">
    <Head>
      <title>Nextjs OAuth with GitHub</title>
      <link rel="icon" href="/favicon.ico" />
    </Head>

    <man>
      <h1>Welcome to Nextjs OAuth with GitHub</h1>
     {
       !props.authorization &&  <a href={"http://localhost:3001/auth/github"} >Click here to login</a>
     }
    </man>
  </div>
)

Home.getInitialProps = (ctx) => {

  const { authorization } = parseCookies(ctx);
  const {token} = ctx.query
  return {
    authorization: authorization || token,
  };
}

export default Home

Now after successful login, the login should no show because during the server side rendering process, we find the cookie and return in as a prop to the component to be used in any subsequent requests to the API. Lets use the token to get the user profile.

Now we know that the cookie is available during server side rendering an so we can use it to do API calls that require authentication like getting user profile for example.

For this we add an endpoint to the user profile Create routes/profile.js

const express = require('express');
const userService = require('../db/services/user')

const router = express()

router.use((req, res, next) => {
    const token = req.headers['authorization'];

    jwt.verify(token, jwtKey, function (err, data) {
        if (err) {
            res.status(401).send({ error: "NotAuthorized" })
        } else {
            req.user = data;
            next();
        }
    })
})

router.get('/', (req, res) => {
    user = await userService.fineById(req.user.id)

    res.send(user);
})

module.exports = router;

And mount on the app.

.....

app.use('/profile', require('./routes/profile'))

// start server
app.listen(3001, () => console.log("Server listening on http://localhost:3001"))

Then we modify the index page to get the user while being server side rendered.

import Head from 'next/head'
import fetch from 'isomorphic-fetch'
import { parseCookies } from 'nookies'

const Home = (props) => (
  <div className="container">
    <Head>
      <title>Nextjs OAuth with GitHub</title>
      <link rel="icon" href="/favicon.ico" />
    </Head>

    <man>
      <h1>Welcome to Nextjs OAuth with GitHub</h1>
      {
        !props.authorization && <a href={"http://localhost:3001/auth/github"} >Click here to login</a>
      }
    </man>
  </div>
)

async function getUser(authorization) {
  const res = await fetch('http://localhost:3001/user')


  if(res.status === 200) return {authorization, user: res.data}
  else return {authorization}
}

Home.getInitialProps = async (ctx) => {

  const { authorization } = parseCookies(ctx);
  const {token} = ctx.query

  const props = getUser(authorization || token)

  return props;
}

export default Home

Conclusion

We have used cookies to manage authentication on to a REST API that uses passportjs to authenticate to GitHub. You can use this method to authenticate to any other OAuth API like Google, Twitter and Facebook.

The entire code used here is on GitHub



Thank you for finding time to read my post. I hope you found this helpful and it was insightful to you. I enjoy creating content like this for knowledge sharing, my own mastery and reference.

If you want to contribute, you can do any or all of the following šŸ˜‰. It will go along way! Thanks again and Cheers!