DEV Community

loading...
Cover image for How to add Auth0 to Nextjs- the Ultimate Guide

How to add Auth0 to Nextjs- the Ultimate Guide

Code Mochi
Hi, I'm Stephen and I love writing about react, javacript, full-stack development, and dev ops.
Originally published at codemochi.com Updated on ・6 min read

In this video, we will show how you can add Auth0 to your Next.js application. This is something that used to be quite tricky but after the release of the api feature in Next.js which makes it super easy to add your own api-only endpoints to a Next.js application, the task of adding Auth0 became a lot more manageable. This became dead simple after Auth0 created a auth-nextjs package which we will use in this video to set up all of our api endpoints.

Github Starter Project

First, go ahead and clone our Next.js Auth0 Starter project on github. Make sure to checkout the start-here tag or you will end up with the finished project. I'm adding a -b development flag so that you will check out the tag and immediately create a development branch so that you won't be in a dreaded detached HEAD state. Feel free to name the branch whatever you'd like:

git checkout start-here -b development

Video of what we are doing:

Let’s first start by installing the @auth0/nextjs-auth0 package.

npm install @auth0/nextjs-auth0 --save

Next, we want to initialize auth0 on the server so that we can use it in all of our api methods. Make sure to define all of the variables in a .env file in the root of your project. You should be able to find the clientId, clientSecret, and domain from auth0 once you create a new single-page application project. Make sure to add the redirect Uri and the postLogoutRedirectUri urls into the respective fields within your auth0 app so that auth0 knows which urls that it should trust during the entire redirect process.

utils/auth.ts

import { initAuth0 } from '@auth0/nextjs-auth0'

export default initAuth0({
  domain: process.env.domain,
  clientId: process.env.clientId,
  clientSecret: process.env.clientSecret,
  scope: 'openid profile',
  redirectUri: process.env.redirectUri,
  postLogoutRedirectUri: process.env.postLogoutRedirectUri,
  session: {
    cookieSecret: process.env.cookieSecret,
    cookieLifetime: 60 * 60 * 8,
    storeIdToken: false,
    storeAccessToken: false,
    storeRefreshToken: false,
  },
  oidcClient: {
    httpTimeout: 2500,
    clockTolerance: 10000,
  },
})
domain=your-auth0-domain.auth0.com
clientId=your-client-id
clientSecret=your-client-secret
redirectUri=http://localhost:3000/api/callback
postLogoutRedirectUri=http://localhost:3000/
cookieSecret=here-is-a-really-long-string-please-use-a-unique-one

In order for the .env variables to be recognized by Next.js we have one more step. Create a next.config.js file and add the following to it:

next.config.js

require('dotenv').config()

module.exports = {}

Make sure to restart your next.js development server after you do this or these settings won't get loaded in. Then, add dotenv to the project:

npm install --save dotenv

This will load the environmental variables from the .env into our app but they will only be accessible from the server, which is exactly what we want. Now let's go ahead and make our api routes. They are all very similar except we will be calling different methods from the auth0 instance that we are defining above.

pages/api/login.ts

import auth0 from '../../utils/auth0'

export default async function login(req, res) {
  try {
    await auth0.handleLogin(req, res, {})
  } catch (error) {
    console.error(error)
    res.status(error.status || 500).end(error.message)
  }
}

pages/api/callback.ts

import auth0 from '../../utils/auth0'

export default async function callback(req, res) {
  try {
    await auth0.handleCallback(req, res, {})
  } catch (error) {
    console.error(error)
    res.status(error.status || 500).end(error.message)
  }
}

pages/api/me.ts

import auth0 from '../../utils/auth0'

export default async function me(req, res) {
  try {
    await auth0.handleProfile(req, res, {})
  } catch (error) {
    console.error(error)
    res.status(error.status || 500).end(error.message)
  }
}

pages/api/logout.ts

import auth0 from '../../utils/auth0'

export default async function logout(req, res) {
  try {
    await auth0.handleLogout(req, res)
  } catch (error) {
    console.error(error)
    res.status(error.status || 500).end(error.message)
  }
}

We start by defining an async function that has a req and a res. This is similar to express middlewares where we get access to the request through the req and modify the response by writing over pieces of the res object. In our case, we have a try/catch block where we try to call the auth0 method and pass the req and res into it. In the event that an error occurs, we log the error and return the error status and the error message.

Next, we need to create a way to store the user state on the client. The following code is pulled directly from the auth-nextjs example folder. The general idea is that we create a react context that will store the user profile information. We will create a provider component called UserProvider which we will have in the root of our project and we will call the useFetchUser react hook in any of the nested react components that need access to the user profile information.

utils/user.tsx

import React from 'react'
import fetch from 'isomorphic-unfetch'

// Use a global to save the user, so we don't have to fetch it again after page navigations
let userState

const User = React.createContext({ user: null, loading: false })

export const fetchUser = async () => {
  if (userState !== undefined) {
    return userState
  }

  const res = await fetch('/api/me')
  userState = res.ok ? await res.json() : null
  return userState
}

export const UserProvider = ({ value, children }) => {
  const { user } = value

  // If the user was fetched in SSR add it to userState so we don't fetch it again
  React.useEffect(() => {
    if (!userState && user) {
      userState = user
    }
  }, [])

  return <User.Provider value={value}>{children}</User.Provider>
}

export const useUser = () => React.useContext(User)

export const useFetchUser = () => {
  const [data, setUser] = React.useState({
    user: userState || null,
    loading: userState === undefined,
  })

  React.useEffect(() => {
    if (userState !== undefined) {
      return
    }

    let isMounted = true

    fetchUser().then(user => {
      // Only set the user if the component is still mounted
      if (isMounted) {
        setUser({ user, loading: false })
      }
    })

    return () => {
      isMounted = false
    }
  }, [userState])

  return data
}

Let's now update the MainLayout component to add the UserProvider. Since the MainLayout was a class-based component we need to also convert it to a functional component because hooks require functional components to function. We will also call the useFetchUser hook here so that we can feed the user and loading boolean into the provider itself.

components/layout/MainLayout.tsx

import { Layout } from 'antd'
import { ReactNode, Component } from 'react'
import Navbar from './Navbar'
import styled from 'styled-components'
import { UserProvider, useFetchUser } from '../../utils/user'

const { Content } = Layout

const StyledContent = styled(Content)`
  min-height: 100vh;
`

export const MainLayout = ({ children }: { children: ReactNode }) => {
  const { user, loading } = useFetchUser()
  return (
    <UserProvider value={{ user, loading }}>
      <Layout>
        <Navbar />
        <StyledContent>{children}</StyledContent>
      </Layout>
    </UserProvider>
  )
}

We are now ready to update the Navbar component to add the useFetchUser hook and use the user object that we get back as a way to tell if the user is logged in or not. If it is undefined we can assume the user is not logged in an display the login button. Otherwise, if there is a user object then we know they are logged in and we can display the logout and profile buttons:

components/layout/Navbar.tsx

import { Layout, Menu } from 'antd'
import Link from 'next/link'
import styled from 'styled-components'
import { useFetchUser } from '../../utils/user'

const { Header } = Layout

const StyledHeader = styled(Header)`
  background-color: #dddbe8;
  .ant-menu {
    width: 100%;
    background-color: #dddbe8;
    a {
      height: 64px;
    }
  }
`

const Navbar = () => {
  const { user, loading } = useFetchUser()

  return (
    <StyledHeader>
      <Menu mode="horizontal">
        <Menu.Item key="/">
          <Link href="/">
            <a>Home</a>
          </Link>
        </Menu.Item>
        {user && !loading
          ? [
              <Menu.Item key="/api/logout">
                <Link href="/api/logout">
                  <a>Logout</a>
                </Link>
              </Menu.Item>,
              <Menu.Item key="/profile">
                <Link href="/profile">
                  <a>Profile</a>
                </Link>
              </Menu.Item>,
            ]
          : null}
        {!user && !loading ? (
          <Menu.Item key="/api/login">
            <Link href="/api/login">
              <a>Login</a>
            </Link>
          </Menu.Item>
        ) : null}
      </Menu>
    </StyledHeader>
  )
}

export default Navbar

Finally, let's update the profile page so that we can display the user's profile information if they are logged in. Otherwise, we will redirect them to the home page.

pages/profile.tsx

import { MainLayout } from '../components/layout/MainLayout'
import styled from 'styled-components'
import { useFetchUser } from '../utils/user'
import Router from 'next/router'

const StyledProfile = styled.div`
  padding: 50px 10px;
  text-align: center;
  h1 {
    font-size: 60px;
  }
`

export default function Profile() {
  const { user, loading } = useFetchUser()

  if (loading) {
    return (
      <MainLayout>
        <p>Loading...</p>
      </MainLayout>
    )
  }
  if (!user && !loading) {
    Router.replace('/')
  }

  return (
    <MainLayout>
      <StyledProfile>
        <h1>🤸</h1>
        <p>Welcome to the Profile Page! Here is your profile information:</p>
        <p>{JSON.stringify(user)}</p>
      </StyledProfile>
    </MainLayout>
  )
}

That's it! You should now have a working Next.js application with Auth0 and are now ready to build the next great web application with this as a starter. Cheers!

If you liked this tutorial, I created an entire course that teaches you how to build a recipe sharing application from the ground up using Next.js, Auth0 and a graphQL CMS called GraphCMS. We will go through how to take the foundation that you learned here and build a fully-featured application that has file uploads, user permissions, responsive design with Ant Design, and the entire app is hosted serverlessly using Zeit's Now service so that you can deploy it with one command and be confident that it will be reliable and scalable no matter how many people visit the page.

Frontend Serverless with React and GraphQL

Frontend Serverless with React and GraphQL

Discussion (4)

Collapse
madza profile image
Madza

I believe in NextJS future potential, seems promising

Collapse
codemochi profile image
Code Mochi Author

Ooh it is! If you haven't tried it out and you love, react.js you really should give it a shot. It is amazing how it takes all of the tricky things in react like page routing, server side rendering and serverless deployment and solves all the tough parts for you.

Collapse
matamatanot profile image
matamatanot

fix typo ,please.

pages/api/profile.ss -> pages/api/logout.ts

Collapse
codemochi profile image
Code Mochi Author

Thank you all fixed! :)