NextAuth.js Intro [3 of 3]: Adding Access Control
In this series we’re exploring different patterns for using next-auth with Next.js for easy and powerful login and sign up for your app. In this next installment we’ll talk about requiring the user to be logged in to see parts of your app.
Links:
- GitHub Repo
- Interactive demo on Vercel
Other articles in this series:
- NextAuth.js: One-Click Signup [Part 1 of 3]
- NextAuth.js: Magic Link Email Authentication [Part 2 of 3]
In most apps, there are features and screens for which we want to require the users to be logged in. For our example app, we’ll assume the user needs some kind of personal dashboard they’ll see only when logged in (since, we can’t know whose data to put on the dashboard unless we know that (1) they’re logged in and (2) who they are),
Create a restricted page
First we’ll create a dashboard page for our app with some content in it. We’ll require that a user log in to see this page.
In /pages/dashboard.js
we'll add some fake-but-neat-looking sample content using Chakra-UI. This can be any sample content, you don’t have to use Chakra-UI. (But if you do, make sure to install it first naturally.)
export default function Dashboard() { return ( <Flex direction='column' align='center' justifyContent='center' minHeight='80vh' > <Heading mb={3}>My Dashboard</Heading> <Stack> <Badge mb={2}>Things</Badge> <Badge mb={2} colorScheme='green'> Stuff </Badge> <Badge mb={2} colorScheme='red'> Foo </Badge> <Badge mb={2} colorScheme='purple'> Bar </Badge> <Badge mb={2} colorScheme='blue'> Baz </Badge> </Stack> </Flex> ); }
Looking good! Chakra-UI is pretty great.
Now let’s do a check to make sure the user is logged in, and redirect otherwise. We’ll do this with 2 great helpers: the getSession()
function from next-auth
, and Next.js's getServerSideProps
function.
export async function getServerSideProps({ req, res }) { const session = await getSession({ req }); if (!session) { return { redirect: { destination: '/', permanent: false, }, } } return { props: {}, }; }
Next.js’s getServerSideProps
function is executed on every page load, before rendering the page. As a convention, it will allow you to return a redirect object to tell the browser to redirect
to another page. Here we call getSession to check that a user's logged in, and return the redirect object if not. If they are logged in, we return an empty props object which will pass through to the page renderer seamlessly.
Restricting a page by hiding it
The above works well if the user knows the address of the page, and is trying to see it without being logged in. This is an effective measure, since there’s no way they can get in.
But why show them a link to it in the first place? We should only show links to restricted pages when the user is logged in.
Let’s start by adding a Navbar
component to /components/Navbar/index.jsx
:
export const Navbar = ({ children }) => ( <Flex w='100%' bg='green.800' px={5} py={4} color='white' justifyContent='space-between' alignItems='center' > <Flex flexDirection='row' justifyContent='center' alignItems='center'> <Link href='/'>Next.js Auth & Access Control Demo</Link> </Flex> <Box>{children}</Box> </Flex> );
It’s just some styling, and has the ability to pass in children which is important since that makes it fairly composable. That is, we can pass it any children we want and it’ll still work, which also means we can wrap it easily with other components that pass specific children to it.
Now we’ll want a logged in and logged out version of it:
export const LoggedOutNavbar = () => <Navbar />; export const LoggedInNavbar = () => ( <Navbar> <Link href='/dashboard' mr={4}> My Dashboard </Link> <Link onClick={signOut}>Log out</Link> </Navbar> );
The logged in links to the dashboard we created, and also shows a Log out link (which we shouldn’t show when someone is logged in either).
Now how do we get them to show conditionally? Easy. We export another component that takes in the session as a prop. If it exists, we show the logged-in version of the Navbar. Otherwise we show the logged-out version.
export const NavbarWithLoginState = ({ session }) => session ? <LoggedInNavbar /> : <LoggedOutNavbar />;
Then inside our DefaultLayout component we add the NavbarWithLoginState
, and grab the session from the useSession
hook:
import { Box } from '@chakra-ui/react'; import { NavbarWithLoginState } from '../../components/Navbar'; import { useSession } from 'next-auth/client'; export const DefaultLayout = ({ children }) => { const [session] = useSession(); return ( <Box> <NavbarWithLoginState session={session} /> {children} </Box> ); }; export default DefaultLayout;
Boom. Only logged in users will see the dashboard link, so there’s a second layer of protection on the private page.
Logged-out homepage
Logged-in homepage
Adding private data and restricting the API route
So we’re redirecting anyone that views the dashboard in the browser who isn’t logged in. But the dashboard will inevitably need some data, which we’ll have to create an API route for. What if someone were to request that API route directly, without being logged in?
We don’t want people fiddling with that endpoint without having at least logged in first. So our API route will need to check that the user is logged in as well. In general the back-end is even more important to restrict, since that’s where all valuable data will come from. So for security reasons and other reasons, let’s put a redirect there as well.
Let’s add an API route to send us some data:
// pages/api/dashboard.js const data = [ {value: 'Stuff', colorScheme: 'green'}, {value: 'Foo', colorScheme: 'red'}, {value: 'Bar', colorScheme: 'purple'}, {value: 'Baz', colorScheme: 'blue'}, ]; export default (req, res) => { res.statusCode = 200 res.json(data) }
If we hit this endpoint in Postman, we’ll see the data sent back in JSON:
Then we can have the dashboard page use this data with a simple React fetch/setState pattern. The dashboard will look exactly the same, but it will now render the data dynamically.
import React, { useState, useEffect } from 'react' import { Badge, Flex, Heading, Stack } from '@chakra-ui/react'; import { getSession } from 'next-auth/client'; const fetchThings = async () => { const response = await fetch('/api/dashboard', { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', } }); const json = await response.json(); if (!response.ok) { throw json } return json; } export default function Dashboard() { const [things, setThings] = useState([]); useEffect(() => { fetchThings().then(things => { setThings(things) }); }, []) return ( <Flex direction='column' align='center' justifyContent='center' minHeight='80vh' > <Heading mb={3}>My Dashboard</Heading> <Stack> {things.map((thing) => { return ( <Badge mb={2} colorScheme={thing.colorScheme} key={thing.value}> {thing.value} </Badge> ) })} </Stack> </Flex> ); } export async function getServerSideProps({ req, res }) { const session = await getSession({ req }); if (!session) { return { redirect: { destination: '/', permanent: false, }, } } return { props: {}, }; }
Now let’s require a session in the API route. next-auth
makes that easy from an API route as well. We just pull in the getSession
helper from next-auth/client
and pass it our request:
import { getSession } from 'next-auth/client' const data = [ {value: 'Stuff', colorScheme: 'green'}, {value: 'Foo', colorScheme: 'red'}, {value: 'Bar', colorScheme: 'purple'}, {value: 'Baz', colorScheme: 'blue'}, ]; export default async (req, res) => { const session = await getSession({req}); if (!session) { res.redirect(307, '/') return } res.status(200).json(data) }
Now you’ll see below that the postman request will return a 307 for temporary redirects.
NOTE: When testing in Postman, make sure you have automatic redirects turned off in the settings, or you’ll see a bunch of HTML as your response.
Great. Now we’ve locked down the dashboard from both the front-end and the back-end. Go celebrate with your beverage of choice 🎉