Before understanding the auth flow, we need to make clear the how NextJS works
Understanding the 3 types of rendering
First of all you need to understand how next js renders things. There are three main ways:
- Statically render the structure of the page when you compile your project in all the pages that don’t have getServerSideProps.
- Server side render your pages on each request, in case you add getServerSideProps to a page
- Client side render your pages, when you navigate from one page to another (after the first render when your user landed on your site)
Now, once you understand these three ways of rendering, NextJS also introduces a different mindset when thinking authentication.
The core idea is:
Do not check for authentication of your pages on the server. Instead simply protect the api routes that return the private data that you need.
In this way, if you don’t check for authentication on the server, you can prevent adding getServerSideProps to your pages.
And if your pages don’t provide a getServerSideProps, then your site will be blazingly fast, because it will be statically rendered.
Later on the client side, you fetch the data that you need, and show a loading skeleton while it does.
But wait, how does the flow end up?
It ends up like this:
- At compile time you statically generate the structure of the protected page
- When a user requests the page, NextJS serves the structure of the page
- On the client side, fetch requests ping your api for the private data
- While this happens the user has already seen a first paint of your site. But without any private data.
- The client side fetch will be authenticated or not and your api will return the data or a 401 error accordingly
- When the request is done, the client rerenders with the data, or redirects the user to /login, accordingly
What do we need to implement it?
- We need to create a React Context that will hold our user
- We need to make that context available to all of our app, by modifying the custom _app.js page and wrapping the root Component Tag with our auth context provider
- We need to create a Higher Order Component (HOC) that makes use of that context and protects routes accordingly
- We need to alter the default headers of our fetch library (in our case, axios) to include the auth token in all requests
- We need to display something while the data loads (in our case, instead of a spinner, we will implement a skeleton loader)
- We need to show an example of how a data request is done
- We need a backend to return an authentication token when a correct login is done (this is out of the scope of this tutorial, send me an email if you’d like to see how that is done.
Show me the code
First install the dependencies that we will need
yarn add js-cookie axios swr Skeleton
Now, let’s start by the axios api instance. This will define how we connect to our backend. Create a new file called api.js
// api.js
import Axios from "axios";
let urls = {
test: `http://localhost:3334`,
development: 'http://localhost:3333/',
production: 'https://your-production-url.com/'
}
const api = Axios.create({
baseURL: urls[process.env.NODE_ENV],
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
export default api;
Then create an Auth Context. The important thing is to add the default headers to the axios api instance once we find a valid token in the cookies
// contexts/auth.js
import React, { createContext, useState, useContext, useEffect } from 'react'
import Cookies from 'js-cookie'
import Router, { useRouter } from 'next/router'
//api here is an axios instance which has the baseURL set according to the env.
import api from '../services/Api';
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function loadUserFromCookies() {
const token = Cookies.get('token')
if (token) {
console.log("Got a token in the cookies, let's see if it is valid")
api.defaults.headers.Authorization = `Bearer ${token}`
const { data: user } = await api.get('users/me')
if (user) setUser(user);
}
setLoading(false)
}
loadUserFromCookies()
}, [])
const login = async (email, password) => {
const { data: token } = await api.post('auth/login', { email, password })
if (token) {
console.log("Got token")
Cookies.set('token', token, { expires: 60 })
api.defaults.headers.Authorization = `Bearer ${token.token}`
const { data: user } = await api.get('users/me')
setUser(user)
console.log("Got user", user)
}
}
const logout = (email, password) => {
Cookies.remove('token')
setUser(null)
delete api.defaults.headers.Authorization
window.location.pathname = '/login'
}
return (
<AuthContext.Provider value={{ isAuthenticated: !!user, user, login, loading, logout }}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => useContext(AuthContext)
Now, we need to make this content available to all of our app, so wrap the _app.js
file with the AuthProvider
// _app.js
import { AuthProvider } from '../contexts/auth'
import Router from 'next/router'
function MyApp({ Component, pageProps }) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
)
}
export default MyApp
Now, we can create the ProtectRoute HOC like this:
// contexts/auth.js
// append this new bit a the end:
export const ProtectRoute = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading || (!isAuthenticated && window.location.pathname !== '/login')){
return <LoadingScreen />;
}
return children;
};
Finally, you can protect all pages except login by wrapping the _app.js
page like this:
import React from 'react'
import { ProtectRoute } from '../contexts/auth'
function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<ProtectRoute>
<Component {...pageProps} />
</ProtectRoute>
</AuthProvider>
)
}
export default MyApp
Wait, how do I fetch data then?
a good way to do it is to use the context we created earlier and to wait for the user to be available.
We make use of useSWR, a hook that implements the static while revalidate protocol. You can read more about that here
This allows us to only fetch the user private data when the user has been resolved.
import React from 'react'
import Head from '../components/Head.js'
import Router from 'next/router'
import DashboardLayout from '../layouts/DashboardLayout'
import useSWR, { mutate } from 'swr'
import api from '../services/Api'
import PageInfo from '../components/PageInfo'
import useAuth, { ProtectRoute } from '../contexts/auth.js'
import Skeleton from 'react-loading-skeleton';
function Dashboard() {
const { user, loading } = useAuth();
const { data: { data: pages } = {}, isValidating } = useSWR(loading ? false : '/pages', api.get)
const showSkeleton = isValidating || loading
return (
<>
<Head>
<title>
Dashboard | MarsJupyter
</title>
</Head>
<div>
<DashboardLayout>
<div className="row">
<div className="col-md-12">
<h1 test-id="dashboard-title">
These are your pages
</h1>
<br />
<table className="table table-responsive-md">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Shareable Link</th>
<th scope="col">Created</th>
<th scope="col">Responses</th>
<th scope="col">Edit</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
{pages && pages.map((page, index) => (
<PageInfo position={index} key={index} page={page}>
{page.slug}
</PageInfo>
))}
</tbody>
</table>
{showSkeleton && <Skeleton height={40} count={5} />}
</div>
</div>
</DashboardLayout>
</div>
</>
)
}
export default ProtectRoute(Dashboard);
As you can see, not only we are able to serve the statically generated structure, but we can also show a loading skeleton while the pages get fetched from the api.
Here is how it looks like when you refresh the page:
As you can see, the rendering is super fast and the “These are your pages” doesn’t even flicker.
UPDATE: Here’s a repo that has the auth logic already set up for you. It also comes with an AdonisJS backend and some pretty cool typescript stuff (the backend types automatically flow to the frontend). But you can always ripoff the backend part and use whichever backend you prefer
Great post! Do you have any thoughts on avoiding the wrapping as a HOC (in the case that most routes already need auth protection)? For instance having something like this: https://medium.com/@tafka_labs/auth-redirect-in-nextjs-3a3a524c0a06 – Here they modify MyApp so most pages get protection by default (besides / itself):
Hello Anil!
I’ve read that article. There are two main points that I’d discuss:
* I think that it is calling
getUser()
for every route change and that will introduce an unnecessary load to your app.I believe you should fetch the user only once on login.
* Regarding avoiding the HOC to wrap all the routes. You could simply wrap
_app.js
and in the wrapper check ifwindow.location.pathname
is in a givenAllowList
(a simple array defining the routes allowed for guests), then skip the protectionGreat answer!