Next.js 12.1.x Methods for Securing Page with NextAuth.js

Overview

This post will review different approaches for securing pages and getting a user’s credentials with NextAuth.js. The article covers securing both single and multiple pages under a folder. The code snippets below will cover some of each approach’s pros and cons. The GitHub solution has a fully functional Next.js application with authentication using credentials.

Important! Changes for Middleware in Next.js Version 12.2.x

The example below was written for Next.js 12.1.x. For Next.js 12.2.x and above, the rules for _middleware.tshave changed. _middleware.ts is now middleware.ts without the underscore and it must be placed at the root of the application. If it is anywhere under the ‘/pages folder, you will get a nested middleware error on build time. I will post the solution on this post as soon as possible. Otherwise, run the following npm command on your Next.js application to keep the example below from breaking.

npm install next@12.1.6

OR

Make sure the ‘\package.json’ file has the following version.

"next": "12.1.6",

Get the Current Version of Next.js

Use the following CLI command to get the current version of Next.js,

npx next -v

How to Run the Example From GitHub

API

Change into the API directory and start the app. The API is configured to run with ‘npm start

cd API
npm start

Next.js App

Change into the ‘nextjs‘ directory and run the application with ‘npm run dev

cd nextjs
npm run dev

Related Article

Prerequisites

If you are not using the source files above, get the Next.js template from MUI. You might have to clone the entire repository to get it, but it is worth the effort. The template from MUI has everything configured out of the box and is ready to use. If you are installing Material UI yourself, it can be challenging unless you are comfortable with Next.js.

https://github.com/mui/material-ui/tree/master/examples/nextjs

Next-Auth library.

npm install next-auth

Regardless if you use typescript and/or @types/node, next-auth requires them to run. Also the middleware file is written in TypeScript.

npm install --save-dev typescript @types/node

Environment Files

Set up a local and production environment files. ‘.env.local’ and ‘.env.production’.

NEXTAUTH_URL = YOUR_DOMAIN
NEXTAUTH_SECRET = YOUR_SECRET_KEY

How to Secure One or More Pages

Using Server Side Rendering to get the Session – Single Page

The example below takes advantage of server side rendering. We can get the session data before the page begins to render, thus removing the delay of calling a synchronous function. Using getServerSideProps() to get the session and then pass it to the component.

Code for the example

import { getSession } from "next-auth/react"
...

export default function UserProfilePage({session}) {

React.useEffect(() => {                          
     if(!Boolean(session)) {
        // Fail - Not Authenticated                     
     }   
     else {   

        // Success - Is Authenticated

     }                
 
  }, [])


 if (session) {
    return (
      <> 
     </>
    )
  }
  else return <Typography variant="h3" align="center"> Access Denied </Typography>

}



export async function getServerSideProps(context) {
    
  return {
    props: {      
      session: await getSession(context)
    },
  };
}

Get the Session on the Client Side

Getting the session after getServerSideProps() will have a delay. So in the example below it will flash “Access Denied” if you do not handle the delay.

import { useSession} from "next-auth/react"
...

export default function UserProfilePage() {

const { data: session, status } = useSession()

React.useEffect(() => {                          
     if(!Boolean(session)) {
        // Fail - Not Authenticated                     
     }   
     else {   

        // Success - Is Authenticated

     }                
  
  }, [])


 if (session) {
    return (
      <> 
     </>
    )
  }

  else return <Typography variant="h3" align="center"> Access Denied </Typography>
}

useEffect() to get the Session

Getting the session after getServerSideProps() will have a delay. The example below will also flash “Access Denied” if you do not handle the delay.

import { getSession } from "next-auth/react"
...

export default function UserProfilePage() {


React.useEffect(() => {             
     const session = await getSession()                     

     if(!Boolean(session)) {
        // Fail - Not Authenticated                     
     }   
     else {   

        // Success - Is Authenticated

     }                
  
  }, [])


 if (session) {
    return (
      <> 
     </>
    )
  }

  else return <Typography variant="h3" align="center"> Access Denied </Typography>
}

Middleware Implementation for Securing Multiple Pages

Taking advantage of Next.js Middleware we can use getToken() to check if the user is authenticated. With getToken(), we can inspect the JWT token Next Auth creates when a user successfully authenticates.

Place the _middelware.js file at the root of a folder to secure one or more pages. Example below shows the _middelware.js file at the root of a folder labeled “.secure”. Any page under the “.secure” folder will be redirected back to the login page if the user is not authenticated.

For Next.js 12.1.x

The file middelware.ts will be in the folder you want to secure. In this example it is under ‘.\pages\auth’

Below is the source code for _middelware.ts.

import { getToken } from 'next-auth/jwt';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  
  const session = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  
  if (session === null) {

    const url = req.nextUrl.clone();
    url.pathname = '/login';
    return NextResponse.redirect(url);
  }
  
  return NextResponse.next();      
}

Changes for Next.js 12.2.x

Changes for version 12.2.x. There can only be one middleware file. The route(s) you want to secure is defined in the file. middelware.ts must be in the application root folder.

Below is a simple example of securing the folder labeled 'auth' for middelware.ts. Learn more about this from Next Auth’s documentation.

export { default } from "next-auth/middleware"

export const config = { matcher: ['/auth/:path*'] };

You can read more about Next.js Middleware here.

Profile Page – Example

The Profile page displays user information from the secure session. The page gets the session from getServerSideProps() and passes it to the page. The data is rendered if the user is authenticated, otherwise it will display Not Authorized. The code example below uses Material MUI version 5.x,

You can try to open the page from the toolbar before you authenticate.

If the user is not authenticated it will show Access Denied

Source for the Profile Page

import React from 'react';
import PropTypes from 'prop-types';
import { createTheme } from '@mui/material/styles';
import { purple } from '@mui/material/colors';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Container } from '@mui/material';
import Box from '@mui/material/Box';
import moment from 'moment';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import { getToken } from "next-auth/jwt"
import { useSession, getSession } from "next-auth/react"
import { useRouter } from 'next/router';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import KeyOutlinedIcon from '@mui/icons-material/KeyOutlined';
import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined';
import TokenOutlinedIcon from '@mui/icons-material/TokenOutlined';
import PermIdentityOutlinedIcon from '@mui/icons-material/PermIdentityOutlined';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';


export default function UserProfilePage({data}) {
    
  const router = useRouter();
  const { data: session, status } = useSession()
    
  const [ userEmail, setUserEmail ] = React.useState('');
  const [ userName, setUserName ] = React.useState('');
  const [ tokenExpiration, setTokenExpiration ] = React.useState('');

  React.useEffect(async () => {            
              
     if(!Boolean(session)) {
      //router.push('/login')                          
     }   
     else {   
      setUserEmail(session.user.email)
      setUserName(session.user.name)
      setTokenExpiration(session.expires)
       console.log(session)
     }                
  
  }, [])
  
  // R E N D E R   P A G E

  if (session) {
    return (
      <>
      <Container maxWidth="lg" align="center">
            
        <Grid container spacing={2}>
          <Grid item xs={4}>
            <Card sx={{ maxWidth: 305 }}>           
              
                <CardContent>
                <Typography variant="h4" sx={{mb: 5}}>
                  User Profile
                </Typography>

                <ListItemButton>
                  <ListItemIcon>
                  <PermIdentityOutlinedIcon/>
                  </ListItemIcon>
                  <ListItemText primary={userName} />
                </ListItemButton>

                <ListItemButton>
                  <ListItemIcon>
                  <EmailOutlinedIcon/>
                  </ListItemIcon>
                  <ListItemText primary={userEmail} />
                </ListItemButton>

                <ListItemButton>
                  <ListItemIcon>
                  <TokenOutlinedIcon/>
                  </ListItemIcon>
                  <ListItemText primary={moment(tokenExpiration).format('MM/DD/YYYY hh:mm')} />
                </ListItemButton>

      
                </CardContent>
              </Card>

          </Grid>
          <Grid item xs={4}>
            <Card sx={{ maxWidth: 305 }}>              
                <CardContent>
                  <Typography variant="h4" >
                  Password
                  </Typography>

                  <KeyOutlinedIcon sx={{fontSize: '78px'}}/>
                    
                    <Typography variant="body2" align="left">
                    Make your password stronger, or change it if someone else knows it.
                    </Typography>
                </CardContent>
              </Card>
          </Grid>
          <Grid item xs={4}>
            <Card sx={{ maxWidth: 305 }}>           
              <CardContent>
                <Typography variant="h4" >
                  Settings
                </Typography>

                <SettingsOutlinedIcon sx={{fontSize: '78px'}}/>
                  
                <Typography variant="body2" align="left">
                Personalize your account settings and see how your data is used.
                </Typography>
                
              </CardContent>
            </Card>

          </Grid>
          <Grid item xs={8}>

          </Grid>
        </Grid>
                
        </Container>
      </>
    );
  }
  else return <Typography variant="h3" align="center"> Access Denied </Typography>
    
}

export async function getServerSideProps(context) {
  const { req, res } = context;

  try {    
    const secret = process.env.NEXTAUTH_SECRET
    const token = await getToken({ req, secret })   
    setTimeout(() => {
      console.log('token', token)  
    }, 1000);
    
  } catch (e) {
    console.log('getServerSideProps Token Error')
  }
  
  
  return {
    props: {      
      data: []
    },
  };
}