- Published on
Securing a Sitecore Headless App with Next.js and Iron-Session
- Authors
- Name
- Jorge Lusar
Building headless Sitecore applications with Next.js gives you a modern developer workflow and performance benefits. But when it comes to user authentication and session management, we need to ensure our app is secure, scalable, and easy to maintain.
In this post, we’ll walk through how to secure a Sitecore Headless app with Next.js using iron-session
. We’ll cover:
- Defining the session model
- Creating a
SessionContext
with React Context - Integrating into the Next.js app wrapper (
_app.tsx
) - Using the session inside components
- Wiring session into Sitecore SSR with a custom plugin
1. Defining the Session Model
Create src/models/session.ts
to define the shape of the session and set up iron-session
options.
import { SessionOptions } from 'iron-session';
export interface SessionData {
nameid: string;
email: string;
firstname: string;
lastname: string;
occupation?: string;
country: string;
userType: string;
isLoggedIn: boolean;
}
export const defaultSession: SessionData = {
nameid: '',
email: '',
firstname: '',
lastname: '',
occupation: undefined,
country: '',
userType: '',
isLoggedIn: false,
};
export const sessionOptions: SessionOptions = {
password: process.env.SESSION_SECRET!,
cookieName: process.env.SESSION_COOKIE!,
cookieOptions: {
path: '/',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax',
},
};
Secure cookies (
httpOnly
,sameSite
,secure
in production) + strong typing for your session.
2. Creating the Session Context
Use a simple React Context to make session data available across the app.
import { createContext, useContext } from 'react';
import { SessionData, defaultSession } from 'src/models/session';
const SessionContext = createContext<SessionData>(defaultSession);
export const useSession = () => useContext(SessionContext);
export default SessionContext;
This lightweight context exposes a global hook (useSession
) to read the current session anywhere in the tree.
_app.tsx
3. Integrating into Wire the context provider into the Next.js root so every page can access the session. All AppContext
references are removed.
import { SitecorePageProps } from 'lib/page-props';
import { I18nProvider } from 'next-localization';
import type { AppProps } from 'next/app';
import Bootstrap from 'src/Bootstrap';
import SessionContext from 'src/context/sessionContext';
import { defaultSession } from 'src/models/session';
import 'src/globals.css';
function App({ Component, pageProps }: AppProps<SitecorePageProps>): JSX.Element {
const { dictionary, ...rest } = pageProps;
return (
<SessionContext.Provider value={pageProps.session ?? defaultSession}>
<Bootstrap {...pageProps} />
<I18nProvider lngDict={dictionary} locale={pageProps.locale}>
<Component {...rest} />
</I18nProvider>
</SessionContext.Provider>
);
}
export default App;
4. Using the Session in Components
Once the provider is in place, consuming session data is simple:
import { useSession } from 'src/context/sessionContext';
export default function HeaderUser() {
const session = useSession();
return (
<div>
{session.isLoggedIn ? (
<>
<span>Hi, {session.firstname || session.email}</span>
<button>Sign out</button>
</>
) : (
<button>Sign in</button>
)}
</div>
);
}
5. Wiring Session into Sitecore SSR (Normal Mode Plugin)
To hydrate pageProps.session
during SSR/SSG, use a Sitecore rendering plugin. This integrates iron-session
with Sitecore Layout/Dictionary services and sets props.session
on the server.
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { DictionaryService, LayoutService } from '@sitecore-jss/sitecore-jss-nextjs';
import { dictionaryServiceFactory } from 'lib/dictionary-service-factory';
import { layoutServiceFactory } from 'lib/layout-service-factory';
import { SitecorePageProps } from 'lib/page-props';
import { pathExtractor } from 'lib/extract-path';
import { Plugin, isServerSidePropsContext } from '..';
import { getIronSession } from 'iron-session';
import { sessionOptions, SessionData } from 'src/models/session'
class NormalModePlugin implements Plugin {
private dictionaryServices: Map<string, DictionaryService>;
private layoutServices: Map<string, LayoutService>;
order = 1;
constructor() {
this.dictionaryServices = new Map<string, DictionaryService>();
this.layoutServices = new Map<string, LayoutService>();
}
async exec(props: SitecorePageProps, context: GetServerSidePropsContext | GetStaticPropsContext) {
if (context.preview) return props;
// Get normalized Sitecore item path
const path = pathExtractor.extract(context.params);
// Use context locale if Next.js i18n is configured, otherwise use default site language
props.locale = (context.locale != undefined && context.locale != 'default') ? context.locale : props.site.language;
// Fetch layout data, passing on req/res for SSR
const layoutService = this.getLayoutService(props.site.name);
props.layoutData = await layoutService.fetchLayoutData(
path,
props.locale,
// eslint-disable-next-line prettier/prettier
isServerSidePropsContext(context) ? (context as GetServerSidePropsContext).req : undefined,
isServerSidePropsContext(context) ? (context as GetServerSidePropsContext).res : undefined
);
if (!props.layoutData.sitecore.route) {
// A missing route value signifies an invalid path, so set notFound.
// Our page routes will return this in getStatic/ServerSideProps,
// which will trigger our custom 404 page with proper 404 status code.
// You could perform additional logging here to track these if desired.
props.notFound = true;
}
// Fetch dictionary data if layout data was present
const dictionaryService = this.getDictionaryService(props.site.name);
props.dictionary = await dictionaryService.fetchDictionaryData(props.locale);
// Initialize links to be inserted on the page
props.headLinks = [];
// Set session
if (isServerSidePropsContext(context)) {
const session = await getIronSession<SessionData>(context.req, context.res, sessionOptions);
props.session = session;
}
return props;
}
private getDictionaryService(siteName: string): DictionaryService {
if (this.dictionaryServices.has(siteName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.dictionaryServices.get(siteName)!;
}
const dictionaryService = dictionaryServiceFactory.create(siteName);
this.dictionaryServices.set(siteName, dictionaryService);
return dictionaryService;
}
private getLayoutService(siteName: string): LayoutService {
if (this.layoutServices.has(siteName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.layoutServices.get(siteName)!;
}
const layoutService = layoutServiceFactory.create(siteName);
this.layoutServices.set(siteName, layoutService);
return layoutService;
}
}
export const normalModePlugin = new NormalModePlugin();
This plugin runs during SSR/SSG, hydrates
props.layoutData
andprops.dictionary
, and—crucially—attaches theiron-session
object toprops.session
so your React tree can read it viaSessionContext
.
Environment Variables
Ensure the following are set (e.g., in .env.local
):
SESSION_SECRET=your-very-long-random-string
SESSION_COOKIE=myapp_session
Wrapping Up
By combining iron-session
, Next.js, React Context, and a Sitecore Normal Mode plugin, you get an end‑to‑end secure session story:
- SSR reads the session with
getIronSession
and passes it throughpageProps.session
- App exposes the session via
SessionContext
- Components consume
useSession()
for personalization and UX
This pattern is reusable for authentication, personalization, and any user-specific state that must persist across pages.
👉 Next steps: enforce stronger cookie policies for admin areas, and connect to your identity provider to populate session data server-side.