Reputation: 449
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
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
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
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