Unlock Seamless Authentication: Integrating NextAuth with Sitecore Headless - Part 2 of 3

Learn how to build a fully functional login system with validation and secure submission using NextAuth.js and Sitecore Headless

November 22, 2024

By Karan Patel

Configuration to Interaction

This is the second part of our deep dive into the integration of NextAuth with Sitecore Headless! In the first part, we laid the groundwork by installing NextAuth and configuring it for seamless authentication. But what’s authentication without a user-friendly login experience?

In this blog, we’ll focus on developing a functional login form that ties directly into the configuration we built earlier and behavior of the app post login such as redirects. Missed Part 1? Catch up here to ensure you’re starting on the right foot.

We’ll focus on the following:

  • Sitecore Items
  • Login Form
  • Post Login Actions

Setting Up Sitecore Items

We’re taking a Sitecore first approach here and there is no hard and fast rule to do it this way. You can do code first approach as well but that requires more clarity on how the form will be setup i.e. field name, dictionary items, etc. Also, both can happen simultaneously as well i.e. Front End dev works on the Login Form and Sitecore dev works on the items. It totally depends upon how you want to do it.

To display the form, we’ll need a rendering (JSON Rendering specifically). For the labels like Username or Password, we can use the dictionary items. For validation or error messages, we can go either dictionary items or a template containing fields that will be used as a datasource to the form. To simplify, we’ll use dictionary items only.

RenderingDescription
LoginFormJSON Rendering that will connect with the LoginForm.tsx component
DictionaryValue
loginFormUsernameUsername
loginFormPasswordPassword
loginFormForgotPasswordForgot Password?
loginFormSignInButtonSIGN IN
loginFormRegisterButtonDon’t have an account? Create one now.
userNameValidationPlease enter your username.
emailValidationPlease enter a valid email address.
passwordValidationPlease enter your password.

Keep in mind we’re creating a barebones form. You may end up creating a template that will be used a datasource if it needs more customization like having some form text at the top of the form that is supplied via Rich Text. However, we’re sticking to the core functionality.

Don’t forget to add the rendering to the appropriate placeholder if you’re on headless only. However, if you’re on SXA, you can add this to the available renderings. At this point, you’ve everything you need for the form to be displayed but you can go a step beyond if you’re on SXA. There is an option to create a login page template and you can create a page design which is assigned to the page template. The page design will use a partial design that has the login form rendering attached to it. This way you can have the whole login functionality separate from the normal content pages. To learn more about Page Designs and Partial Designs you can checkout the following articles:

Creating the Login Form

Now that we have setup our Sitecore items, we can go ahead and create our Login Form in Next.js. To construct the form we’ll focus on three things, the form itself, validation and submission. You can go one step more and add some reCAPTCHA verification as well to avoid any suspicious submissions. We’re not focus on that, however, you can checkout our blog on how to integrate Google ReCAPTCHA in Sitecore Next.js Headless App.

Designing the Form

Before we start with form, we’ll setup a hook for the dictionary items. This hook will then be used in our form to get the dictionary items. This is a good practice in order to structure the code better. If you’re having issues with your dictionary items loading up, you can checkout our blog that provides some easy steps to troubleshoot dictionary related issues.

// src/utils/useTranslations.ts

import { useI18n } from 'next-localization';

export function useTranslations() {
  const i18n = useI18n();

  return {

    //LoginFormUsername will be the item in Sitecore with the same key name.
    loginFormUsername: i18n.t('LoginFormUsername'),
    loginFormPassword: i18n.t('LoginFormPassword'),
    loginFormForgotPassword: i18n.t('LoginFormForgotPassword'),
    loginFormSignInButton: i18n.t('LoginFormSignInButton'),
    loginFormRegisterButton: i18n.t('LoginFormRegisterButton'),
    userNameValidation: i18n.t('UserNameValidation'),
    emailValidation: i18n.t('EmailValidation'),
    passwordValidation: i18n.t('PasswordValidation'),
  };
}

Now create a LoginForm.tsx and use the following to create a simple form. Keep in mind, the validation and submission code is missing which can be found below. The classNames are kept empty so you can add and customize it to your own need.

// src/components/forms/authentication/LoginForm.tsx

import React, { useEffect, useState } from 'react';
import { useSitecoreContext, withSitecoreContext } from '@sitecore-jss/sitecore-jss-nextjs';
import { useTranslations } from 'utils/translations';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';

const LoginForm = (): JSX.Element => {

  // States for input fields and error
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState<string | null>(null);

  // Hook for dictionary items
  const translation = useTranslations();

  // Sitecore Context to verify if page is in Edit Mode
  const { sitecoreContext } = useSitecoreContext();
  const isEdit = sitecoreContext?.pageEditing;

  // Hook to check if user is signed in.
  const { data: session, status } = !isEdit ? useSession() : { data: nullm status: null };

  // Hook for handling redirection
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    //Code from Handling Form Submission section
  };

  return (
    <div className="">
      <form className="" onSubmit={handleSubmit} noValidate>
        <h2 className="">Login Form</h2>
        <div className="">
          <label className="">
            {translation.loginFormUsername}
            <span className="" aria-hidden="true">
              *
            </span>
          </label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className=""
            required
          />
        </div>
        <div className="">
          <label className="">
            {translation.loginFormPassword}
            <span className="" aria-hidden="true">
              *
            </span>
          </label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className=""
            required
          />
        </div>
        <div className="">
          <a href="<!--ADD FORGOT PASSWORD LINK-->" className="">
            {translation.loginFormForgotPassword}
          </a>
        </div>

        <button className="" type="submit">
          {translation.loginFormSignInButton}
        </button>
        <hr className="" />

        <button className="" type="button">
          <a href="<!--ADD REGISTER LINK-->">{translation.loginFormRegisterButton}</a>
        </button>
      </form>
      {error && <div className="">{error}</div>}
    </div>
  );
};

export default withSitecoreContext()(LoginForm);

Adding Validation

Its time to add some validation to the form inputs. Validation can be done in two ways, on submit and when the input goes out of focus. We’ve focused on doing the validation when the submit action happens. In order to make the code more readable, we’ll create a separate function that will handle the validation for us.

// function to validate the form (needs to be added within LoginForm.tsx only
  
const validateForm = () => { setError(null);
if (!email) { setError(translation.userNameValidation); return false; }
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) { setError(translation.emailValidation); return false; }
if (!password) { setError(translation.passwordValidation); return false; }
return true; };

Handling Form Submission

Last but not the least, we’ll handle the submission. This is the crucial part which will integrate the form with the NextAuth configuration we did in the first blog. As mentioned previously, we’ll handle validation on submit action. Hence, the first setup would be to call the validateForm function. After that, we’ll call the signIn function from NextAuth which will handle the credentials and login functionality for us.

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  setError(null);
  
if (!validateForm()) return;
//Execute login if form validation is successful const result = await signIn('credentials', { redirect: false, email, password, });
if (result?.error) { setError('Credentials are not correct. Please try again.'); } };

The signIn function is responsible for sending the inputs to the credentials provider we defined in the first blog. You can learn more about the signIn here. The redirect option is only used with credentials and email provider. By setting it to false it would disable the redirect and we would have to handle what happens post login. If you want to simply redirect the user to the homepage after log in, then you can use specify a callbackUrl like below.

signIn('credentials', {
  email,
  password,
  callbackUrl: 'http://localhost:3000/' // modify it according to your site
});

// HINT: You can use the PUBLIC_URL environment variable in order to redirect after successful signin.

By default the callbackUrl needs to be an absolute URL with the same hostname, otherwise it would redirect to the homepage. In order to define some custom redirection, you can use the redirect callback which can support relative URLs.

Adding SessionProvider

In order to utilize the useSession() hook we need to make sure that <SessionProvider> is defined in our _app.tsx. Before we go ahead and modify that we’ll need to extend the SitecorePageProps and add the Session interface to it. This needs to be done as session props are not available inside Sitecore’s page props.

// src/lib/page-props.ts
  
import { DictionaryPhrases, ComponentPropsCollection, LayoutServiceData, SiteInfo, HTMLLink, } from '@sitecore-jss/sitecore-jss-nextjs'; import { Session } from 'next-auth';
// Extend the SitecorePageProps to include the session export type ExtendedSitecorePageProps = SitecorePageProps & { session?: Session | null; }; /** * Sitecore page props */ export type SitecorePageProps = { site: SiteInfo; locale: string; dictionary: DictionaryPhrases; componentProps: ComponentPropsCollection; notFound: boolean; layoutData: LayoutServiceData; headLinks: HTMLLink[]; };
import type { AppProps } from 'next/app';
import { I18nProvider } from 'next-localization';
import { SessionProvider } from 'next-auth/react';
import { ExtendedSitecorePageProps } from 'lib/page-props';

function App({ Component, pageProps }: AppProps<ExtendedSitecorePageProps>): JSX.Element {
const { dictionary, ...rest } = pageProps;
// To check whether page is in Edit Mode. const isEdit = pageProps?.layoutData?.sitecore?.context?.pageEditing;
return ( // Use the next-localization (w/ rosetta) library to provide our translation dictionary to the app. // Note Next.js does not (currently) provide anything for translation, only i18n routing. // If your app is not multilingual, next-localization and references to it can be removed.
// Skip providing SessionProvider when page is in Edit Mode. isEdit ? ( <I18nProvider lngDict={dictionary} locale={pageProps.locale}> <Component {...rest} /> </I18nProvider> ) : ( <SessionProvider session={pageProps.session}> <I18nProvider lngDict={dictionary} locale={pageProps.locale}> <Component {...rest} /> </I18nProvider> </SessionProvider> ) ); }
export default App;

SessionProvider has the following properties that can be configured that will define when the session should be fetched.

PropertyDescription
refetchIntervalA time interval (in seconds) after which the session will be re-fetched. Default is 0 and session in not polled.
refetchOnWindowFocusSessionProvider automatically refetches the session when the user switches between windows. Default is true and to disable auto refetch when switching windows set it to false
refetchWhenOfflineSet to false to stop polling when the device has no internet access offline
// Example for session refetching every 5 minutes and disabling automatic refetch upon window switching

<SessionProvider session={pageProps.session} refetchInterval={300} refetchOnWindowFocus={false}>

Handling Post-Login Actions

We’re not going to use the redirect functionality provided by NextAuth which is why redirect is set to false. Instead we’ll develop our own and this is to facilitate if a user tries to access a protected page (https://www.<your-site>.com/protected) and they’re not logged in, then they’ll be redirected to sign in. After successful sign in, they should be redirected back to /protected.

For now, we’ll simply redirect the user to the homepage and the above functionality will be covered in the another blog as it requires to develop a custom middleware.

// Code to redirect user to homepage after session is found.
  
useEffect(() => { if (session && status === "authenticated") { router.replace('/') } }, [session, status]);

Finishing the Login Form Journey

That’s it folks! We bridged the gap between configuration and functionality by creating a fully interactive login flow. We began by designing the login form, ensuring it provides a seamless user experience. Next, we integrated validation to keep user inputs clean and error-free. Finally, we tackled form submission, connecting our frontend to NextAuth.js for secure authentication.

With these building blocks in place, we have a user-friendly login experience powered by Sitecore Headless and NextAuth but we’re not stopping here. In the next part, we’ll go over writing up a custom middleware to handle redirects. These redirects will ensure a smooth and secure flow for the users i.e. user redirecting to sign in when accessing a member page, prefetch requests are responded with a 301 redirect and much more. Stay tuned and subscribe to our snack newsletter for latest updates.

Karan Developer

Karan Patel

Sitecore Developer

Karan is a Sitecore Certified Developer with over 2 years of experience building and implementing Sitecore solutions using ASP .NET MVC, React, Azure and AWS. He's an avid fan of Counter-Strike and can usually be found playing it if he is not developing. In his spare time, he likes to play guitar and cook Indian food.

Second CTA Ogilvy's Legacy

Today, David Ogilvy's influence can still be felt in the world of advertising.

Ogilvy's Influence Example
Emphasis on research Market research is a crucial part of any successful advertising campaign
Focus on headlines A strong headline can make the difference between an ad that is noticed and one that is ignored
Use of visuals Compelling images and graphics are essential for capturing audience attention