DEV Community

Cover image for Building Google-JWT Authentication with NextAuth
Ifenna Monanu
Ifenna Monanu

Posted on • Edited on

Building Google-JWT Authentication with NextAuth

The time and comfort of the modern man cannot be negotiated. Gone are the days where users go through a long sequence of input fields in order to sign up into a software. This however brought about the innovation of a passwordless onboarding system where time and energy are conserved.

In this article I would work you through a flow process on how to setup user authentication just in two clicks using Next.js and it's authentication library NextAuth.js .

What is Next.js?

Next.js is a framework built on top of React that makes developing production-ready, fully-optimized React apps super fast and easy. One of the main characteristics of Next.js is its run time speed.

Next.js is used in production by top companies like Netflix, Tiktok, and Nike. It is super easy to learn, especially if you’re familiar with React.

What is NextAuth.js?

NextAuth.js is a Next.js authentication library. It exists as an abstraction of the OAuth library as it leverages on its authentication and authorization providers (such as Google, Twitter, Facebook, GitHub). With NextAuth.js a software can have access to a user's profile information on adequate permission by the user.

NextAuth.js has a client-side API you can use to interact with sessions in your app. The session data returned from the Providers contains user payload, and this can be displayed to the user upon successful login.

The session data returned to the client looks like this:



{
  expires: '2055-12-07T09:56:01.450Z';
  user: {
        email: 'newsuer@example.com';
        image: 'https://avatars2.githubusercontent.com/u/45228014?v=4';
        name: 'newuser';
    }
}


Enter fullscreen mode Exit fullscreen mode

Requirements

  • Node.js 10.13 or later installed on your local machine
  • Basics of React.js

Create a new Next.js project

Creating a Next.js app is similar to creating a new react app. At first we created a new directory in our case my_projects in our new created directory we create a new instance of a next app into a the next_auth directory.



mkdir my_projects
cd my_projects
npx create-next-app@latest next_auth


Enter fullscreen mode Exit fullscreen mode

NB: @latest ensures that our installation comes along with the latest Next.js version.

Once this is done an instance of a Next app would be available in our project directory.

Installing Packages

Our next line of action would be to install the necessary packages.



npm i next-auth axios jsonwebtoken --save
or
yarn add next-auth axios jsonwebtoken --save


Enter fullscreen mode Exit fullscreen mode
  • next-auth:- Next.js authentication library the backbone of our authentication build.

  • jsonwebtoken:- used in signing a user payload which could be a user Id or email in order to generate a token necessary for authorization.

  • axios:- with axios we would be making two (2) API calls to our server. The first is to confirm if the authenticating email already exists if yes then it logs in the user, else it calls the second API which is responsible for storing user details in the database. We would look into this more as we proceed.

Upon successful installation, next-auth should be added to the dependencies in your package.json file:



//Dependencies in package.json
"dependencies": {
 "dependencies": {
    "axios": "^0.27.2",
    "jsonwebtoken": "^8.5.1",
    "next": "latest",
    "next-auth": "^4.10.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
  }
}


Enter fullscreen mode Exit fullscreen mode

Create a Google OAuth app

To allow users to log in to our app using their Google account, we have to obtain OAuth 2.0 client credentials from the Google API Console. Navigate to Credentials and click on Create credentials, and then OAuth client ID:

Image description

You would be asked to fill in the following:

  • Choose an Application Type: Select Web Application
  • Name: This is the name of your application
  • Authorized JavaScript origins: This is the full URL to the homepage of our app. Since we are still in development mode, we are going to fill in the full URL our development server is running on. In this case, it is http://localhost:3000.

NB: Remember to change your home page URL once your app gets into production mode.

NB: These URLs are custom URLs none of these them are assigned to our software by google.

Next, a popup will display your client ID and client secret. Copy both keys to your clip board.

Add an environmental variable

Next, create an .env.local file in your project’s root directory. Next.js has inbuilt support for environment variables and .env.local will load those variables to process.env. Therefore, the filename cannot be a random name. For more information, don’t hesitate to read Next.js documentation on environment variables.

Next, populate the newly created file with my OAuth credentials we had copied earlier.



GOOGLE_ID=<client id of your google auth app should go in here>
GOOGLE_SECRET=<client secret of your google auth app should go in here>
NEXTAUTH_URL= http://localhost:3000
NEXT_PUBLIC_JWT_SECRET_KEY: <your jwt secret key>


Enter fullscreen mode Exit fullscreen mode

Google Provider Initialization

Now, back to the app. I created a file named [...nextauth].js in pages/api/auth.

Image description

In the file add the following code:



import NextAuth from 'next-auth'
import GoogleProvider from "next-auth/providers/google";

const options = {
    providers: [
        GoogleProvider({
            clientId: process.env.GOOGLE_ID,
            clientSecret: process.env.GOOGLE_SECRET
        }),
    ],
}


Enter fullscreen mode Exit fullscreen mode

On line 1, I imported NextAuth, which is main package behind the build.

On line 2,I imported Google Provider from the next-auth library,

On line 6, I configured the Google provider and passing in my Google secret and client ID through the environmental variables.

Setup action page

NextAuth provides an option to setup custom pages which would be returned to the browser depending on several line of action tolled during the course of our user authentication.
The pages object is placed directly below the providers array.



const options = {
    providers: [
        GoogleProvider({
            clientId: process.env.GOOGLE_ID,
            clientSecret: process.env.GOOGLE_SECRET
        }),
    ],

    pages: {
    signIn: '/auth/dashbaord', // on successfully signin    
    signOut: '/auth/login', // on signout redirects users to a custom login page.
    error: '/auth/error',  // displays authentication errors
    newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
  }
}


Enter fullscreen mode Exit fullscreen mode

Authentication Callbacks

Callbacks are asynchronous functions used to control what happens when an action is performed. Request to the backend API is handled using callbacks.

NextAuth provides us with certain vital callbacks which include:

  • Sign in callback : Use the signIn() callback to control if a user is allowed to sign in. With in this object we would be making two axios request to our backend API, the first one would be to verify if our user exists, this request would query the database to verify if the user's email exists if yes it returns true signifying that the user want's to login to his account, if the query returns false we would then make our second axios request which receives the user profile object returned from google and stores it in our custom database.

lets see how this really works:-



  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      // first axios request to ascertain if our user exists in our custom DB
      const response = await axios.post(
        "http://localhost:9000/v1/auth/userExists",
        { email: profile.email }
      );
      if (response && response.data?.value === true) {
        // user exists return true passing control to the next callback
        return true;
      } else {
        // second axios call which creates a new user in our database
        const data = {
          firstName: profile.given_name,
          lastName: profile.family_name,
          email: profile.email,
          profileUrl: profile.picture,
        };
        const response = await axios.post(
          "http://localhost:9000/v1/auth/signup",
          data
        );
        // retruns true thereby passing control to the next callback
        return true;
      }
    },
}


Enter fullscreen mode Exit fullscreen mode

- JWT callback : this callback takes in a user object, token object as it's parameters. The user object contains the details of the signed in user, on line 5, SignToken() function is called which returns a jwt token, this token is appended to our callback token object as userToken,

token.userToken = token;



  async jwt({ token, user, account }) {
      if (account) {
        console.log(user, token, account);
        // call the signToken function which returns a JWT token
        const token = await SignToken(user?.email as string);
        token.userToken = token;
      }
// the token object is passed done to the session call back for persistence
      return token;

    },


Enter fullscreen mode Exit fullscreen mode

our SignToken() function:



import jwt from 'jsonwebtoken';

const SignToken = async (email)=> {
const token = await jwt.sign({id:email}, process.env.NEXT_PUBLIC_JWT_SECRET_KEY, {expiresIn: '1d'});
    return token
}

export default SignToken;


Enter fullscreen mode Exit fullscreen mode
  • Session Callback: Session callback takes in the token object passed in from the jwt callback seen above, and a session object. The jwt token is appended to the session object as loggedUser;

session.loggedUser = token.userToken;

The session object get persisted in our browser session for authorization at any point. The session callback is called whenever a session is checked.



   async session({ session, token, user }) {
      // Send properties to the client, like an access_token from a provider.
      session.loggedUser = token.userToken;
      return session;
    },
  },


Enter fullscreen mode Exit fullscreen mode

A glimpse of how the browser's session object now looks like;



{
  expires: '2055-12-07T09:56:01.450Z';
// our jwt token
  loggedUser: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCI1NiIsInR"
  user: {
        email: 'newsuer@example.com';
        image: 'https://avatars2.githubusercontent.com/u/45228014?v=4';
        name: 'newuser';
    }
}



Enter fullscreen mode Exit fullscreen mode

Put together our [...nextauth].js file looks like:



import GoogleProvider from "next-auth/providers/google";
import NextAuth from "next-auth";
import axios from "axios";
import { SignToken } from "../../../utils/siginToken";

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: <string>process.env.GOOGLE_CLIENT_ID,
      clientSecret: <string>process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
 pages: {
    signIn: '/auth/dashbaord',
    signOut: '/auth/login',  
    error: '/auth/error',   
    newUser: '/auth/new-user'
  }
 callbacks: {
    async signIn({ user, account, profile}) {
    const response = await axios.post(
        "http://localhost:9000/v1/auth/userExists",
        { email: profile.email }
      );
      if (response && response.data?.value === true) {        
          return true;
      } else {
        const data = {
          firstName: profile.given_name,
          lastName: profile.family_name,
          email: profile.email,
          profileUrl: profile.picture,
        };
        const response = await axios.post(
          "http://localhost:9000/v1/auth/signup",
          data
        );
        return true;
      }
    },
    async jwt({ token, user, account }) {
      if (account) {         
        const userLoggedIn = await SignToken(user?.email as string);
        token.loggedUser = userLoggedIn;
      }
      return token;
    },
    async session({ session, token, user }) {
      session.loggedUser = token.loggedUser;
      return session;
    },
  },
});



Enter fullscreen mode Exit fullscreen mode

Check user login state and jwt token with the useSession() Hook

Create an _app.js file in your pages directory (if it doesn’t already exist) and add the following code:



import { SessionProvider } from "next-auth/react"
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <SessionProvider session={pageProps.session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

export default MyApp


Enter fullscreen mode Exit fullscreen mode

By wrapping the component in a Session Provider, I enabled session state to be shared between pages. This, in turn, will preserve the state during page navigation, improve performance, and reduce network traffic.

User Sign In / Sign Out Buttons

Next, open the components/Header.js file and import useSession, signIn, and signOut from next-auth/client:

import { useSession, signIn, signOut } from 'next-auth/react'

useSession will be used to manage the sign in and sign out state of our users, while signIn and signOut will be used to perform the login and logout features in our app.

Let’s make use of the useSession Hook:

const { data: session } = useSession();

The session will return the user’s details. Let’s use the details returned to conditionally render a sign in and sign out button.

With in the body of our Header.js file we would append the signin / signout conditional logic:



<div className='header'>
      <Link href='/'>
        <a className='logo'>NextAuth.js</a>
      </Link>
           {session && <a href="#" onClick={handleSignout} className="btn-signin">Sign out</a>  } 
           {!session && <a href="#" onClick={handleSignin}  className="btn-signin">Sign in</a>  } 
    </div>


Enter fullscreen mode Exit fullscreen mode

We need to create the handleSignin and handleSignout methods to enable our users to sign in and sign out:



const handleSignin = (e) => {
      e.preventDefault()
      signIn()
  }    
const handleSignout = (e) => {
      e.preventDefault()
      signOut()
    }


Enter fullscreen mode Exit fullscreen mode

My browser display once after the click of the signin button:

Image description

Your Header.js should now look like this:



import Link from 'next/link'
import { useSession, signIn, signOut } from 'next-auth/react'

export default function Header () {
  const { data: session } = useSession();

  const handleSignin = (e) => {
    e.preventDefault()
    signIn()
  }

  const handleSignout = (e) => {
    e.preventDefault()
    signOut()
  }

  return (
    <div className='header'>
      <Link href='/'>
        <a className='logo'>NextAuth.js</a>
      </Link>
      {session && <a href="#" onClick={handleSignout} className="btn-signin">Sign out</a>  }
      {!session && <a href="#" onClick={handleSignin}  className="btn-signin">Sign in</a>  }
    </div>
  )
}



Enter fullscreen mode Exit fullscreen mode

Retrieve and display user information

Now, onto our pages/index.js. I need to display and conditionally render user details based on their login details. First, I imported the useSession hook from next-auth.

So, replace your index.js with the following content:



import Head from 'next/head'
import Header from '../components/Header'
import styles from '../styles/Home.module.css'
import { useSession } from 'next-auth/react'

export default function Home() {
  const { data: session, status } = useSession()
  const loading = status === "loading"

  return (
    <div className={styles.container}>
      <Head>
        <title>Nextjs | Next-Auth</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header />
      <main className={styles.main}>
        <h1 className={styles.title}>Authentication in Next.js app using Next-Auth</h1>
        <div className={styles.user}>
           {loading && <div className={styles.title}>Loading...</div>}
           {
            session &&
              <>
               <p style={{ marginBottom: '10px' }}> Welcome, {session.user.name ?? session.user.email}</p> <br />
               <img src={session.user.image} alt="" className={styles.avatar} />
              </>
            }
           {
            !session &&
              <>
               <p className={styles.title}>Please Sign in</p>

              </>
           }
         </div>
      </main>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

NB: In order gain access to some backend authorized API's you can now access your jwt token appended in user session. Which can now be appended to the header of an authorized API call.



import { useSession } from 'next-auth/react

export default function JWT() {
  const { data: session, status } = useSession()

  // jwt_token 
  const jwt_token = session.loggedUser;

  return {token}
}


Enter fullscreen mode Exit fullscreen mode

in the above hook I created a react hook used to return an authenticated user's token.

Conclusion

Through this article we have been able to understand the development flow process of google authentication using NextAuth library. We further explained how we could interact with a custom backend API using axios as well as JSON web token for authorizing users.

if you did like to know more about Next.js kindly checkout their documentation at https://nextjs.org/docs

more details on NextAuth.js, how to integrate other authentication Strategies like GitHub, Twitter and others can be seen in their documentation at: https://next-auth.js.org/getting-started/introduction

Top comments (1)

Collapse
 
shivapasunuri profile image
shivapasunuri

Git Hub url?