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.
Rendering | Description |
---|---|
LoginForm | JSON Rendering that will connect with the LoginForm.tsx component |
Dictionary | Value |
---|---|
loginFormUsername | Username |
loginFormPassword | Password |
loginFormForgotPassword | Forgot Password? |
loginFormSignInButton | SIGN IN |
loginFormRegisterButton | Don’t have an account? Create one now. |
userNameValidation | Please enter your username. |
emailValidation | Please enter a valid email address. |
passwordValidation | Please 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.
Property | Description |
---|---|
refetchInterval | A time interval (in seconds) after which the session will be re-fetched. Default is 0 and session in not polled. |
refetchOnWindowFocus | SessionProvider 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 |
refetchWhenOffline | Set 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.