Create a HOC (higher order component) for authentication in Next.js

10,369

Solution 1

You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.

For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.

export function requireAuthentication(gssp) {
    return async (context) => {
        const { req, res } = context;
        const token = req.cookies.userToken;

        if (!token) {
            // Redirect to login page
            return {
                redirect: {
                    destination: '/admin/login',
                    statusCode: 302
                }
            };
        }

        return await gssp(context); // Continue on to call `getServerSideProps` logic
    }
}

You would then use it in your page by wrapping the getServerSideProps function.

// pages/index.js (or some other page)

export const getServerSideProps = requireAuthentication(context => {
    // Your normal `getServerSideProps` code here
})

Solution 2

Based on Julio's answer, I made it work for iron-session:

import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '@/utils/index'

export const withAuth = (gssp: any) => {
    return async (context: GetServerSidePropsContext) => {
        const { req } = context
        const user = req.session.user

        if (!user) {
            return {
                redirect: {
                    destination: '/',
                    statusCode: 302,
                },
            }
        }

        return await gssp(context)
    }
}

export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))

And then I use it like:

export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
    return {
        props: {},
    }
})

My withSessionSsr function looks like:

import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'

const IRON_OPTIONS: IronSessionOptions = {
    cookieName: process.env.IRON_COOKIE_NAME,
    password: process.env.IRON_PASSWORD,
    ttl: 60 * 2,
}

function withSessionRoute(handler: NextApiHandler) {
    return withIronSessionApiRoute(handler, IRON_OPTIONS)
}

// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
    handler: (
        context: GetServerSidePropsContext
    ) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
    return withIronSessionSsr(handler, IRON_OPTIONS)
}

export { withSessionRoute, withSessionSsr }
Share:
10,369
aleksander frnczak
Author by

aleksander frnczak

Updated on July 22, 2022

Comments

  • aleksander frnczak
    aleksander frnczak almost 2 years

    So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.

    The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.

    This is my HOC code: (cookie are stored under userToken name)

    import React from 'react';
    const jwt = require('jsonwebtoken');
    
    const RequireAuthentication = (WrappedComponent) => {
    
      return WrappedComponent;
    };
    
    
    export async function getServerSideProps({req,res}) {
      const token = req.cookies.userToken || null;
    
    // no token so i take user  to login page
      if (!token) {
          res.statusCode = 302;
          res.setHeader('Location', '/admin/login')
          return {props: {}}
      } else {
        // we have token so i return nothing without changing location
           return;
         }
    }
    export default RequireAuthentication;
    

    If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.

  • aleksander frnczak
    aleksander frnczak about 3 years
    Thanks for the help! Although I have another problem, i can't get cookies in "getServerSideProps", i tried with req.cokies.userToken, with npm packages like nookies or cookie and nothing is working. I get the context object but when i access req.cookies a get and empty object event if i see that cookie on the browser
  • aleksander frnczak
    aleksander frnczak about 3 years
    i didn't get this issue before because i didn't get so far with my solution so i think it's related to this
  • aleksander frnczak
    aleksander frnczak about 3 years
    Okay, i even don't have the cookie object in my req.headers co i just start a new thread because im not sure if this is related to your answer, but thank you very much for help!
  • juliomalves
    juliomalves about 3 years
    The HOC will just pass what's sent to it so shouldn't be an issue in itself. I've tested it locally, and I can see cookies being passed in req.headers.cookie inside the HOC.
  • carlosbvz
    carlosbvz over 2 years
    This seems to be a nice approach for dynamic pages, dunno how to make it work for static pages (using getStaticProps). Any thoughts?
  • juliomalves
    juliomalves over 2 years
    If you're using static pages the authentication needs to happen on the client-side. getStaticProps runs at build time, and has no access to request-specific information like cookies. That being said, you could create some kind of custom hook (to be re-used) to deal with it on the client-side.
  • deadcoder0904
    deadcoder0904 over 2 years
    @juliomalves can you add ts version? i'm unable to type gssp? i got context: GetServerSidePropsContext working well but don't know how to type gssp. i went with any. furthermore, i get an error saying TypeError: Cannot read property 'user' of undefined, how do i solve it?
  • juliomalves
    juliomalves over 2 years
    @deadcoder0904 Typing gssp as requireAuthentication(gssp: GetServerSideProps) works for me. Regarding the error you're getting, you may want to create a new question for that as I don't think it's related to this question.
  • deadcoder0904
    deadcoder0904 over 2 years
    @juliomalves it was an error bcz i wasn't wrapping ironSessionSsr so when i did that, it worked but typing gssp as GetServerSideProps doesn't work for me, sadly :(
  • juliomalves
    juliomalves over 2 years
    @deadcoder0904 You may want to create a new question for the typing issue you're having.
  • deadcoder0904
    deadcoder0904 over 2 years
    @juliomalves i would've but it isn't a significant of a problem. using any for now :)
  • procul
    procul about 2 years
    is there a way to use this approach if I don't need to call getServerSideProps from the protected page?
  • juliomalves
    juliomalves about 2 years
    @procul If you're not using getServerSideProps is the authentication happening on the client then? For that you can have a look at the client-side solution proposed in stackoverflow.com/a/70659746/1870780.
  • procul
    procul about 2 years
    The authentication is still happening on the the server-side; however, I don't need to add additional server-side data fetching for some of the protected pages. The approach above seems to be a higher-order function for the gssp for the page, which means that it can't really be used for a page unless it has some some additional server-side fethching? Or am I mistaken?
  • juliomalves
    juliomalves about 2 years
    @procul You don't have to add any other server-side fetching if you don't want to. You could remove the gssp param from requireAuthentication(gssp) and refactor the HOC code accordingly if you don't want that. You'll probably still have to return an empty props object from the HOC though.
  • procul
    procul about 2 years
    That makes sense - thanks!!
  • agahpars
    agahpars almost 2 years
    This is what I have been searching for 3 hours. Thanks!