aleksander frnczak
aleksander frnczak

Reputation: 449

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

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.

Upvotes: 9

Views: 26291

Answers (3)

Oskar Hertzman
Oskar Hertzman

Reputation: 321

For anybody using TRPC, and want to use a HOC for auth logic, this is our approach, notice that we only create helpers once:

import type {
  GetServerSidePropsContext,
  NextApiRequest,
  NextApiResponse,
} from "next";
import superjson from "superjson";
import type { DehydratedState } from "@tanstack/query-core";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";

// Create a function to instantiate helpers
const createHelpers = async (context: GetServerSidePropsContext) => {
  return createServerSideHelpers({
    router: appRouter,
    ctx: await createContext({
      req: context.req as NextApiRequest,
      res: context.res as NextApiResponse,
    }),
    transformer: superjson,
  });
};
export const withAuth = (
  gssp: (
    context: GetServerSidePropsContext,
    helpers: Awaited<ReturnType<typeof createHelpers>>,
  ) => Promise<GetServerSidePropsResult<{ [key: string]: any }>>,
) => {
  return async (context: GetServerSidePropsContext) => {
    const helpers = await createHelpers(context);

    const { req } = context;
    const user = await helpers.users.sessionUser.fetch();

    if (!user) {
      return {
        redirect: {
          destination: "/sign-in",
          permanent: false,
        },
      };
    }

    if (req.url?.includes("/admin")) {
      if (!user.admin) {
        return {
          notFound: true,
        };
      }
    }
    return gssp(context, helpers);
  };
};


// USAGE

 export const getServerSideProps = withAuth(async (_context, helpers) => {
  await helpers.campaigns.list.prefetch({});

  return {
    props: {
      trpcState: helpers.dehydrate(),
    },
  };
});

Upvotes: 1

deadcoder0904
deadcoder0904

Reputation: 8693

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 }

Upvotes: 3

juliomalves
juliomalves

Reputation: 50368

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
})

Upvotes: 39

Related Questions