Create a HOC (higher order component) for authentication in Next.js - node.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.

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

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 }

Related

How can I set a flash variable in Next.js before a redirect?

Laravel in PHP made this easy with https://laravel.com/docs/9.x/session#flash-data, so I figured Next.js would have an easy way too.
I thought I'd be able to do something like:
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getSession(ctx);
if (!session) {
ctx.res.setHeader("yourFlashVariable", "yourFlashValue");
console.log('headers', ctx.res.getHeaders()); // Why is it not even appearing here?
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
const props = ...
return { props };
};
and then in my other page:
export const getServerSideProps: GetServerSideProps = async (context) => {
const { headers, rawHeaders } = context.req;
// look inside the headers for the variable
// ...
But the header doesn't appear.
If you know how to achieve the goal of a flash variable (even if not using headers), I'm interested in whatever approach.
(Originally I asked How can I show a toast notification when redirecting due to lack of session using Next-Auth in Next.js? but now feel like I should have asked this more generic question.)
UPDATE
I appreciate the reasonable suggestion from https://stackoverflow.com/a/72210574/470749 so have tried it.
Unfortunately, index.tsx still does not get any value from getFlash.
// getFlash.ts
import { Session } from 'next-session/lib/types';
export default function getFlash(session: Session) {
// If there's a flash message, transfer it to a context, then clear it.
const { flash = null } = session;
console.log({ flash });
// eslint-disable-next-line no-param-reassign
delete session.flash;
return flash;
}
// getNextSession.ts
import nextSession from 'next-session';
export default nextSession();
// foo.tsx
import { getSession } from 'next-auth/react';
import { GetServerSideProps, InferGetServerSidePropsType, NextApiRequest, NextApiResponse } from 'next';
import getNextSession from '../helpers/getNextSession';
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getSession(ctx);
if (!session) {
const req = ctx.req as NextApiRequest;
const res = ctx.res as NextApiResponse;
const nSession = await getNextSession(req, res);
nSession.flash = 'You must be logged in to access this page.'; // THIS LINE CAUSES A WARNING
console.log({ nSession });
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
// ...
return { props };
};
// index.tsx
import { GetServerSideProps } from 'next';
import getFlash from '../helpers/getFlash';
import getNextSession from '../helpers/getNextSession';
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getNextSession(context.req, context.res);
let toast = getFlash(session);
console.log({ toast });
if (!toast) {
toast = 'no toast';
}
console.log({ toast });
return {
props: { toast }, // will be passed to the page component as props
};
};
Also, the nSession.flash = line causes this warning:
warn - You should not access 'res' after getServerSideProps resolves.
Read more: https://nextjs.org/docs/messages/gssp-no-mutating-res
Your first code is working fine for me (printing the headers in terminal). However, the combination will not work as intended because the headers you set in /foo (say) will be sent to browser, along with a status code of 307, and a location header of /. Now "the browser" will be redirecting to the location and it won't forward your headers. Similar threads: https://stackoverflow.com/a/30683594, https://stackoverflow.com/a/12883411.
To overcome this, you can do something like this. This works because the browser does send the cookies (in this case, set when you create a session).
// lib/session.ts
import type { IronSessionOptions } from 'iron-session'
import type { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
export const sessionOptions: IronSessionOptions = {
password: process.env.SECRET_COOKIE_PASSWORD as string,
cookieName: 'sid',
cookieOptions: { secure: process.env.NODE_ENV === 'production' },
}
declare module 'iron-session' {
interface IronSessionData {
flash?: string | undefined
}
}
export const withSessionRoute = (handler: NextApiHandler) =>
withIronSessionApiRoute(handler, sessionOptions)
export const withSessionSsr = <P extends Record<string, unknown> = Record<string, unknown>>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) => withIronSessionSsr(handler, sessionOptions)
// pages/protected.tsx
import type { NextPage } from 'next'
import { getSession } from 'next-auth/react'
import { withSessionSsr } from 'lib/session'
const ProtectedPage: NextPage = () => <h1>Protected Page</h1>
const getServerSideProps = withSessionSsr(async ({ req, res }) => {
const session = await getSession({ req })
if (!session) {
req.session.flash = 'You must be logged in to access this page.'
await req.session.save()
return { redirect: { destination: '/', permanent: false } }
}
return { props: {} }
})
export default ProtectedPage
export { getServerSideProps }
// pages/index.tsx
import type { InferGetServerSidePropsType, NextPage } from 'next'
import { withSessionSsr } from 'lib/session'
const IndexPage: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ flash }) => {
// TODO: use `flash`
}
const getServerSideProps = withSessionSsr(async ({ req }) => {
// if there's a flash message, transfer
// it to a context, then clear it
// (extract this to a separate function for ease)
const { flash = null } = req.session
delete req.session.flash
await req.session.save()
return { props: { flash } }
})
export default IndexPage
export { getServerSideProps }
This also works if you want to set flash data in an API route instead of pages:
import { withSessionRoute } from 'lib/session'
const handler = withSessionRoute(async (req, res) => {
req.session.flash = 'Test'
await req.session.save()
res.redirect(307, '/')
})
export default handler
Complete example: https://github.com/brc-dd/next-flash/tree/with-iron-session

Get the current Firebase user from Next.js getServerSideProps

I'm looking for how to get the currently signed in Firebase user with Next.js through getServerSideProps. I've already tried using verifyIdAuthToken but I can't seem to change the persistence type through Firebase to get the cookie because I get this error:
[t [Error]: The current environment does not support the specified persistence type.] {
code: 'auth/unsupported-persistence-type',
a: null
}
My current code is this:
// utils/firebaseClient.js
import firebase from 'firebase';
const firebaseConfig = {
// redacted
}
if(!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
} else {
firebase.app();
}
const auth = firebase.auth();
const googleProvider = new firebase.auth.GoogleAuthProvider();
export { firebase, googleProvider };
// pages/dashboard.js
import { firebaseAdmin } from '../utils/firebaseAdmin';
import nookies from 'nookies';
export default function Dashboard({ user }) {
if (!user) {
return (
<>
<h1>Loading</h1>
</>
)
}
return (
<>
<h1>Hi</h1>
</>
);
}
export async function getServerSideProps(ctx) {
console.log(nookies.get(ctx));
try {
const cookies = nookies.get(ctx);
console.log(cookies);
const user = await firebaseAdmin.auth().verifyIdToken(cookies.token);
} catch (err) {
console.error(err);
return {
redirect: {
destination: '/',
permanent: false
}
}
}
return {}
}
// pages/index.js
import firebase from 'firebase';
import { googleProvider } from '../utils/firebaseClient';
export default function Authentication() {
firebase.auth().onAuthStateChanged((user) => {
if(user) window.location.replace('/dashboard');
});
const googleButton = () => {
firebase.auth().signInWithPopup(googleProvider).then((result) => {
window.location.href = '/dashboard';
}).catch((err) => {
return console.log(err);
});
}
return (
<>
<button onClick={googleButton}>Login with Google</button>
</>
)
}
Does anyone know how I can fix this, or if there is an alternate way to get the current Firebase user in getServerSideProps?
By the way, I have already read how to get firebase use in nextJS getserversideprops and the solution didn't work for me. It also seems like it didn't work for the author of that question. Thanks in advance!

Using next-iron-session's "withIronSession" with Next.JS to perform simple authentication

So I have a very simple Next.js application that I would like to add authentication to using next-iron-session and the withIronSession function. Inside of my api folder, I have a simple component that grabs the login form data, performs a complex authentication action, then sets the user using
req.session.set("user", { email });
import { withIronSession } from "next-iron-session";
const VALID_EMAIL = "user";
const VALID_PASSWORD = "password";
export default withIronSession(
async (req, res) => {
if (req.method === "POST") {
const { email, password } = req.body;
if (email === VALID_EMAIL && password === VALID_PASSWORD) {
req.session.set("user", { email });
await req.session.save();
console.log(req.session.get());
return res.status(201).send("");
}
return res.status(403).send("");
}
return res.status(404).send("");
},
{
cookieName: "MYSITECOOKIE",
password: '2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8'
}
);
After the user is set in the session, I return a 201, and the client redirects to my protected page, where I try to grab that user in the getServerSideProps function using the withIronSession as a wrapper.
import { withIronSession } from "next-iron-session";
export const getServerSideProps = withIronSession(
async ({ req, res }) => {
const user = req.session.get("user");
console.log(user);
if (!user) {
res.statusCode = 403;
res.end();
return { props: {} };
}
return {
props: { }
};
},
{
cookieName: 'MYSITECOOKIE',
password: "2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8"
}
);
However, even though the user is found in the API function after it is set, ({"user", { email }}), that same session object returns {} in the getServerSideProps function in my protected component, which in my case always results in a 403. Is there a way to access the user that is set in the login component in the getServerSideProps function?
Note*
I was trying to follow this article, maybe it provides more information.
https://dev.to/chrsgrrtt/easy-user-authentication-with-next-js-18oe
next-iron-session works with node version > 12. It worked for me.
import React from "react";
import { withIronSession } from "next-iron-session";
const PrivatePage = ({ user }) => (
<div>
<h1>Hello {user.email}</h1>
<p>Secret things live here...</p>
</div>
);
export const getServerSideProps = withIronSession(
async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
res.statusCode = 404;
res.end();
return { props: {} };
}
return {
props: { user }
};
},
{
cookieName: "MYSITECOOKIE",
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false
},
password: process.env.APPLICATION_SECRET
}
);
export default PrivatePage;
I believe that this happens only on an http origin. Are you testing on localhost or any other non secure origin? If yes, then you need to specify secure: false in the cookieOptions.
From the documentation of next-iron-session, it is set to true by default, and this means that only secure origins (typically, origins with https) will be allowed.
Consider updating cookieOptions with secure: false for development environment and secure: true for production.
Like this:
withIronSession(
async ({ req, res }) => {...},
{
cookieName: 'MYSITECOOKIE',
cookieOptions: {
secure: process.env.NODE_ENV === 'production' ? true : false
},
password: '2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8'
}
);
I faced the same problem & found a simple solution:
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 }

User is not available in request with NestJS passport strategy (other than documentation exemples)

I'm trying to implement a passport strategy (passport-headerapikey), I was able to make it work and I can secure my routes.
But the request is empty and cannot access the logged in user ?
import { HeaderAPIKeyStrategy } from "passport-headerapikey";
import { PassportStrategy } from "#nestjs/passport";
import { Injectable, NotFoundException } from "#nestjs/common";
import { CompanyService } from "../../companies/companies.service";
#Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, "api-key") {
constructor(private readonly companyService: CompanyService) {
super(
{
header: "Authorization",
prefix: "Api-Key "
},
true,
async (apiKey, done) => {
return this.validate(apiKey, done);
}
);
}
public async validate(apiKey: string, done: (error: Error, data) => {}) {
const company = await this.companyService.findByApiKey(apiKey);
if (company === null) {
throw new NotFoundException("Company not found");
}
return company;
}
}
#UseGuards(AuthGuard("api-key"))
export class CompaniesController {
constructor(private companyService: CompanyService) {}
#Get()
#ApiOperation({ title: "Get company information" })
public getCompany(#Request() req) {
// here request is empty, so i cannot access the user..
console.log("request", req);
return [];
}
}
Thanks for your help !
To access the logged user, you can inject the object in the request. To do that, in your ApiKeyStrategy constructor, change the third parameter to something like this:
async (apiKey, verified, req) => {
const user = await this.findUser(apiKey);
// inject the user in the request
req.user = user || null;
return verified(null, user || false);
}
Now, you can access the logged user:
getCompany(#Request() req) {
console.log(req.user);
}
I hope that could help you.
As show in the documentation you should do some works to get the current user : here the documetation
First of all in the app.module make sure that the context is set :
context: ({ req }) => ({ req })
Then you can add this in the controller/resolver, this example use the Gql (GraphQL):
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
if this one doesnt work for you try this one instead :
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext();
request.body = ctx.getArgs();
return request.user;
},
);
Modify your validate method like so:
public async validate(apiKey: string, done: (error: Error, data) => {}) {
const company = await this.companyService.findByApiKey(apiKey);
if (company === null) {
return done(new NotFoundException("Company not found"), null);
}
return done(null, company);
}

Call server-side function from ReactJS component

I'm trying to implement a payments system in my ReactJS app that requires server-side code.
I have several questions:
How do you connect a ReactJS app so it can communicate with server-side code?
How would you set up a function in the server-side code?
How would you call that function from a component in a ReactJS app?
For reference, I'm trying to integrate Stripe subscriptions. They give server-side code examples for Node, PHP, etc.
FYI: I am not trying to set up server-side rendering. When you search for server-side code in reference to ReactJS, that's just about all that comes up.
EDIT: I'm particularly interested in a NodeJS solution. I'm also using Webpack.
Just in case, it is helpful to you... I have a React UI that triggers video processing on a Django backend (I mainly use GraphQL through Apollo Client to trigger my server side functions and REST framework when file transfers are involved).
Is REST an option for you?
The middleware I use for file transfers for example:
const SERVER_URL = process.env.SERVER_URL;
const fileTransferApi = (payload) => {
const { authenticated, token, endpoint, body, contentType, method } = payload;
let config = {};
if (authenticated) {
if (token) {
config = {
method,
headers: {
'Content-Type': contentType,
Authorization: `Bearer ${token}`
},
body
};
} else {
throw new Error('No token saved!');
}
}
return fetch(`${SERVER_URL}/api/rest/v1/${endpoint}`, config)
.then((response) =>
response.text().then((text) => ({ text, response }))
).then(({ text, response }) => {
if (!response.ok) {
return Promise.reject(text);
}
return text;
}).catch((err) => console.log(err));
};
export const FILE_TRANSFER_API = Symbol('FILE_TRANSFER_API');
export default () => (next) => (action) => {
const fileTransferApiAction = action[FILE_TRANSFER_API];
if (typeof fileTransferApiAction === 'undefined') {
return next(action);
}
const { payload, types } = fileTransferApiAction;
const [, successType, errorType] = types;
return fileTransferApi(payload).then(
(response) =>
next({
type: successType,
payload: {
text: response,
message: 'ok'
}
}),
(error) => next({
type: errorType,
payload: {
error: error.message || 'There was an error.'
}
})
);
};
My store (I use Redux):
import { createStore, compose, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import ReduxThunk from 'redux-thunk';
import ApolloClientSingleton from '../network/apollo-client-singleton';
import fileTransferApi from '../middlewares/fileTransferApi';
import reducer from './reducers';
export default class Store {
constructor(history, initialState = {}) {
this.data = createStore(
reducer,
initialState,
compose(
applyMiddleware(
fileTransferApi,
ReduxThunk.withExtraArgument(ApolloClientSingleton),
routerMiddleware(history),
ApolloClientSingleton.middleware()
),
typeof window === 'object' && typeof window.devToolsExtension !== 'undefined'
? window.devToolsExtension() : (f) => f
)
);
}
}
In my actions:
export const windowsDownload = (authenticated, token) => ({
[FILE_TRANSFER_API]: {
types: [WINDOW_DOWNLOAD_REQUEST, WINDOW_DOWNLOAD_SUCCESS, WINDOW_DOWNLOAD_FAILURE],
payload: {
endpoint: 'file_transfer/download/windows',
contentType: 'text/csv',
method: 'get',
body: null,
authenticated,
token
}
}
});
This REST setup enables me to send requests (POST video, GET csv...) from my React UI to my Django server. Can't you set up some REST calls between your app and your server?

Resources