Logo
Published on

Securing a Sitecore Headless App with Next.js and Iron-Session

Authors
  • avatar
    Name
    Jorge Lusar
    Twitter

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:

  1. Defining the session model
  2. Creating a SessionContext with React Context
  3. Integrating into the Next.js app wrapper (_app.tsx)
  4. Using the session inside components
  5. 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.

src/models/session.ts
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.

src/context/sessionContext.tsx
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.


3. Integrating into _app.tsx

Wire the context provider into the Next.js root so every page can access the session. All AppContext references are removed.

src/pages/_app.tsx
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.

src/lib/page-props-factory/plugins/normal-mode.ts
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 and props.dictionary, and—crucially—attaches the iron-session object to props.session so your React tree can read it via SessionContext.


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 through pageProps.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.