Build cookie based auth for multi-tenant NextJS application

Varun Raj
Varun Raj, Co-founder and CTO

We’ve been looking to adopt NextJS stack for our web app as the primary client-side framework for several reasons, even though the framework looked promising, we felt there could be a bunch of challenges moving from our traditional React app that was rendered by Rails asset pipeline with the react-rails gem.

One of the challenges was the authentication module as it needs to involve cookie sessions to make server-rendered pages and client-side session/storage to make browser API calls.

As the database and core logic of the app still lies with Rails we had to use the NextJS server as only a middleware that can hold cookie data and render pages and scripts. Our approach was mainly to make an API call to the Rails server with the credential and retrieve a JWT Token which will then be stored in the NextJS cookie and used for making future calls.

Next-Auth Library

Initially, when we did the basic research about the NextJS framework, we found that the solid approach for authentication is to use the Next Auth library as it has Built-In implementation for almost all the OAuth providers out on the internet.

As we moved forward with Next Auth, we had quite a few flexibility issues as it is bundled well and good to work in a project that has next itself as the back end. Also, one primary issue we faced with Next auth is that it doesn’t support multi-tenant infrastructure, which means applications that has a subdomain and also custom domains for each of their customers cannot use NextAuth, and why is this? Because the cookie domain is set at the build/compile time rather than at the run time.

Refer to this bug report on nextjs/auth for more info.

Once we figured that out, I felt it’s high time that we build our authentication setup for a stack that has an API for validation and NextJS for session management.

To keep it simple and reliable I followed some of the best practices of Next Auth and built my own.

The Step Up

The first problem to solve is to set up a cookie setter and getter method, as mentioned earlier for us the key requirement is that the domain of the cookie should be dynamic based on the request/params and also open for subdomains in case of our domain.

Handling Cookie
export const serializeCookie = (name, value, req, options = {}) => {
const { origin } = req.headers
const domain = origin ? getDomain(origin) : null // Gets the full domain from origin
const { maindomain } = domain ? getSubdomain(domain) : { maindomain: null } // Gets the subdomain and main domain
return cookie.serialize(name, String(value), {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'none',
secure: process.env.NODE_ENV === 'production' ? true : false,
path: '/',
domain: maindomain ? '.' + maindomain : null,
...options
})
}

The above function works as the setter method for the cookie and this will be called from our NextJS API endpoints. Also, this has the flexibility of setting custom options on the fly. For instance, if a cookie that you use in the app has less expiry time, then you can set that and override the default option I defined here.

Similarly, the getter method is pretty straightforward

Handling Cookie
export const getCookie = (req, name) => {
let cookieData = cookie.parse(req.headers.cookie || '')
if (cookieData && cookieData[name]) {
return cookieData[name]
}
return null
}

With this, my sign-in API endpoint looks like below, where I’m forwarding the data I get from UI to my back end via post-call and then setting the token I get from response with the setHeader function of the next response and getting the cookie from my custom function.

Handling Cookie
const login = (req, res) => {
const { body } = req
return Api.post(BACKEND_SIGN_IN, body)
.then(data => {
res.setHeader(
'Set-Cookie',
serializeCookie(HN_SESSION_KEY, data.token, req, {})
)
return res.json(data)
})
.catch(err => res.status(400).json({ message: err.message }))
}
const signup = (req, res) => {
const { body } = req
return Api.post(BACKEND_SIGN_UP, body)
.then(data => {
res.setHeader(
'Set-Cookie',
serializeCookie(HN_SESSION_KEY, data.token, req, {})
)
return res.json(data)
})
.catch(err => res.status(400).json({ message: err.message }))
}

While in both sign in and sign up I didn’t send any custom options to my cookie setter method, while this comes handy when I write the logout method. Here I delete the cookie by overriding the maxAge option.

Handling Cookie
const signout = (req, res) => {
res.setHeader(
'Set-Cookie',
serializeCookie(HN_SESSION_KEY, '', req, { maxAge: 0 })
)
res.status(200).json({ success: true })
}

Managing Authentication on Client-Side

The cookie we set is only a server-side cookie for obvious security reasons, but now since most of our applications will be generated and rendered on the client-side we need to give a hint to the front end that if the current session is an authenticated one or not.

For this, I used an approach that the Next Auth library was using. Which is to wrap our root react component with a SessionProvider component and within-session provider make an API Call to my NextJS back end and fetch the token from the header.

Handling Cookie
import { getSession } from '@/models/Auth'
import React, { useState, useEffect } from 'react'
import SessionContext from './SessionContext'
export default function SessionProvider({ children }) {
const [session, setSession] = useState(null)
const [loading, setLoading] = useState(true)
const [errorMessage, setErrorMessage] = useState(null)
const fetchSession = () => {
setLoading(true)
getSession()
.then(_session => setSession(_session))
.catch(err => setErrorMessage(err.message))
.then(() => setLoading(false))
}
useEffect(() => {
fetchSession()
}, [])
if (errorMessage) return <p>{errorMessage}</p>
const status = loading ? 'loading' : session ? 'loggedIn' : 'loggedOut'
return (
<SessionContext.Provider value={{ status, ...session }}>
<SessionContext.Consumer>{ctx => children}</SessionContext.Consumer>
</SessionContext.Provider>
)
}

Here the getSession method makes an API call to /api/user/session and get the token in payload if its a authenticated session

Handling Cookie
const session = (req, res) => {
let session = getCookie(req, HN_SESSION_KEY)
if (session) {
return res.json({ session })
} else {
res.json({ session: null })
}
}

Once the data is set to the context, this can be accessed within any page/component. For simplicity in code, we wrote a useSession hook, exactly like next auth to get the session and the status

Handling Cookie
import { useState, useEffect, useContext } from 'react'
import SessionContext from './SessionContext'
export default useSession = () => {
const session = useContext(SessionContext)
return session
}

Now wrap you application component with SessionProvider in \_app.js to set context globally before any other API Calls.

_app.js
<SessionProvider session={session} refetchInterval={5 * 5 * 60}>
<Authenticator>
<Component {...pageProps} />
</Authenticator>
</SessionProvider>

If you noticed I created another parent for my page component which is the Authenticator component. This is pretty crucial to set the token in the header for all. the subsequent API calls to my Rails back end.

In the Authenticator component, I get the session and set it to Axios default headers so that all the further API calls will go with the authentication headers

Handling Cookie
export default function Authenticator({ children }) {
const { session, status } = useSession()
useEffect(() => {
if (status === 'loading') return
axios.defaults.headers.common['Authorization'] = session
}, [status])
if (status === 'loading') return <p>Loading...</p>
return children
}

With this approach, the entire control over setting and usage of a cookie is with our control which earlier with Next Auth wasn’t the case.

Hope this article helped to understand more about cookie handling in your NextJS Application.