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 managable. 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/profile.ss
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 middlewheress 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 accesss 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 its 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 permisssions, 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.