I recently got the chance to implement SAP Customer Data Cloud (formerly Gigya) authentication in our Sitecore XM Cloud Next.js application. The goal was to create an authentication system that integrates with Sitecore's headless architecture while providing secure JWT-based authentication, session management, and SSO capabilities.
While SAP CDC is a powerful CIAM platform, integrating it with Next.js and Sitecore presented several unique challenges—from JWT verification complexities to session persistence issues and middleware configuration. This post documents the complete implementation, the challenges we encountered, and how we solved them.
Architecture Overview
The authentication flow combines SAP CDC's CIAM platform with NextAuth.js to provide JWT-based authentication for our Sitecore application. Here's how it works:
- User Authentication: Users authenticate through SAP CDC's screen-sets (customizable login UI)
- JWT Generation: SAP CDC generates a JWT token after successful authentication
- Token Verification: NextAuth verifies the JWT using SAP CDC's public key
- Session Management: NextAuth creates and manages server-side sessions
- Route Protection: Next.js middleware protects Sitecore routes based on session state
This approach allows us to leverage SAP CDC's authentication capabilities while maintaining compatibility with Sitecore's headless architecture and Next.js routing.
The Challenges We Faced
Challenge 1: JWT Verification with Dynamic Public Keys
Problem: SAP CDC uses RSA256 JWT tokens, but the public key must be fetched dynamically from their API. Initially, we tried to cache the public key, but this caused issues when SAP CDC rotated keys.
Solution: We implemented a JWT verification system that fetches the public key on-demand and handles key rotation gracefully:
async verifyIdToken(idToken: string) {
try {
// Fetch SAP CDC public key dynamically
const gigyaUsApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_US_API_BASE_URL || 'https://accounts.us1.gigya.com';
const pubKeyResp = await fetch(
`${gigyaUsApiBaseUrl}/accounts.getJWTPublicKey?apiKey=${process.env.NEXT_PUBLIC_GIGYA_API_KEY}&format=json`
);
const pubKeyData = await pubKeyResp.json();
if (pubKeyData.errorCode && pubKeyData.errorCode !== 0) {
throw new Error('Failed to fetch SAP CDC public key');
}
const { n, e } = pubKeyData;
if (!n || !e) throw new Error('Public key data is incomplete');
// Verify JWT using jose library
const jwk = { kty: 'RSA', n, e };
const key = await jose.importJWK(jwk, 'RS256');
const { payload } = await jose.jwtVerify(idToken, key);
return payload;
} catch (error) {
console.error('SAP CDC JWT verification failed:', error);
throw error;
}
}
Challenge 2: Session Persistence Across Page Reloads
Problem: Users were being logged out on page refresh, even though their SAP CDC session was still valid. The issue was that NextAuth wasn't properly syncing with the SAP CDC session state.
Solution: We implemented a session verification mechanism that checks SAP CDC session status before showing the login screen:
function verifyGigyaSession(): Promise<boolean> {
return new Promise((resolve) => {
const w = window as any;
if (!w.gigya?.accounts?.getAccountInfo) return resolve(false);
w.gigya.accounts.getAccountInfo({
callback: (res: any) => resolve(res.errorCode === 0),
});
});
}
Then in the login page, we check for existing sessions before showing the login form:
useEffect(() => {
loadGigyaScript()
.then(async () => {
const hasSession = await verifyGigyaSession();
if (hasSession) {
// User already has SAP CDC session, establish NextAuth session
const w = window as any;
w.gigya?.accounts?.getJWT({
fields: 'profile,email,emails,data',
expiration: 600,
callback: async (jwtRes: any) => {
if (jwtRes.errorCode === 0 && jwtRes.id_token) {
const result = await establishAppSession(jwtRes.id_token);
if (result.ok) {
window.location.href = callbackUrl || '/my-account';
}
}
},
});
return;
}
showScreenSet();
});
}, []);
Challenge 3: Middleware Route Protection
Problem: Protecting Sitecore routes with middleware while maintaining compatibility with Sitecore's JSS middleware was tricky. We needed to ensure authentication checks happened at the right time without breaking Sitecore's routing.
Solution: We created a middleware that runs after Sitecore's JSS middleware and checks for authentication tokens:
export async function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
// Define protected Sitecore routes
const protectedRoutes = ['/my-account', '/profile', '/settings'];
const isProtectedRoute = protectedRoutes.some(route => pathname.startsWith(route));
if (isProtectedRoute) {
const token = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!token) {
const url = req.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(url);
}
}
return NextResponse.next();
}
Challenge 4: SDK Loading Race Conditions
Problem: The SAP CDC SDK is loaded asynchronously, and we were trying to use it before it was ready, causing "gigya is not defined" errors.
Solution: We implemented a proper SDK loading mechanism with Promise-based initialization:
function loadGigyaScript() {
return new Promise<void>((resolve, reject) => {
if ((window as any).gigya) return resolve();
const script = document.createElement('script');
const gigyaCdnBaseUrl = process.env.NEXT_PUBLIC_GIGYA_CDN_BASE_URL || 'https://cdns.us1.gigya.com';
script.src = `${gigyaCdnBaseUrl}/js/gigya.js?apiKey=${GIGYA_API_KEY}`;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load SAP CDC SDK'));
document.body.appendChild(script);
});
}
Challenge 5: Logout Synchronization
Problem: Logging out from NextAuth didn't clear the SAP CDC session, and vice versa, leading to inconsistent authentication state.
Solution: We created a unified logout function that clears both sessions:
export async function logoutFromGigya(): Promise<void> {
return new Promise<void>((resolve) => {
try {
const gigyaWindow = window as any;
if (typeof window !== 'undefined' && gigyaWindow?.gigya?.accounts?.logout) {
gigyaWindow.gigya.accounts.logout({
callback: () => {
console.log('SAP CDC logout successful');
resolve();
},
});
} else {
resolve();
}
} catch (error) {
console.error('Error during SAP CDC logout:', error);
resolve();
}
});
}
// In logout component
const handleSignOut = async () => {
await logoutFromGigya(); // Clear SAP CDC session first
await signOut({ callbackUrl: '/login', redirect: false });
window.location.href = '/login';
};
Prerequisites
What you need:
- Sitecore XM Cloud instance with JSS Next.js app
- SAP CDC account (API Key, User Key, Secret Key)
- Node.js 18+, Next.js 14+
Get your SAP CDC credentials:
- Log into SAP CDC Console → Admin → Site Settings → API Keys
- Note your API Key, User Key, and Secret Key
Implementation Guide
Step 1: Install Dependencies
npm install next-auth jose
Create .env.local in your Sitecore Next.js app root:
# NextAuth (generate secret: openssl rand -base64 32)
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_secret_here
NEXTAUTH_JWT_SECRET=your_jwt_secret_here
# SAP CDC Public (from SAP CDC Console)
NEXT_PUBLIC_GIGYA_API_KEY=your_api_key
NEXT_PUBLIC_GIGYA_SCREENSET=Default-RegistrationLogin
NEXT_PUBLIC_GIGYA_START_SCREEN=gigya-login-screen
# SAP CDC Data Center (adjust us1/eu1/au1 based on region)
NEXT_PUBLIC_GIGYA_CDN_BASE_URL=https://cdns.us1.gigya.com
NEXT_PUBLIC_GIGYA_API_BASE_URL=https://accounts.gigya.com
NEXT_PUBLIC_GIGYA_US_API_BASE_URL=
# SAP CDC Private (server-side only - KEEP SECRET)
GIGYA_USER_KEY=your_user_key
GIGYA_SECRET=your_secret
# Optional
NEXT_PUBLIC_SESSION_TIMEOUT_MINUTES=30
⚠️ Important: Add .env.local to .gitignore!
Step 3: Extend NextAuth Types
Create src/types/next-auth.d.ts:
import { DefaultSession } from 'next-auth';
declare module 'next-auth' {
interface Session extends DefaultSession {
gigyaProfile?: Record<string, unknown>;
cardNumber?: string; // Replace with your Sitecore user identifier
gigyaToken?: string;
isFirstLogin?: boolean;
}
interface User {
gigyaProfile?: Record<string, unknown>;
cardNumber?: string;
gigyaToken?: string;
isFirstLogin?: boolean;
}
}
declare module 'next-auth/jwt' {
interface JWT {
gigyaProfile?: Record<string, unknown>;
cardNumber?: string;
gigyaToken?: string;
isFirstLogin?: boolean;
}
}
Step 4: Create SAP CDC Service
Create src/lib/gigya/gigya-service.ts:
/* eslint-disable @typescript-eslint/no-explicit-any */
interface GigyaConfig {
apiKey: string;
userKey: string;
secret: string;
}
interface GigyaAccountInfo {
UID: string;
profile?: { firstName?: string; lastName?: string; email?: string };
data?: { CardNumber?: string; FirstLogin?: boolean }; // Adjust for your Sitecore schema
loginIDs?: { emails?: string[] };
errorCode?: number;
errorMessage?: string;
}
class GigyaService {
private config: GigyaConfig;
constructor(config: GigyaConfig) {
this.config = config;
}
async getAccountInfo(uid: string): Promise<GigyaAccountInfo | null> {
try {
const formData = new URLSearchParams();
formData.append('apiKey', this.config.apiKey);
formData.append('userKey', this.config.userKey);
formData.append('secret', this.config.secret);
formData.append('UID', uid);
formData.append('include', 'emails,data,profile,loginIDs');
const gigyaApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_API_BASE_URL || 'https://accounts.gigya.com';
const response = await fetch(`${gigyaApiBaseUrl}/accounts.getAccountInfo`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
});
const result = await response.json();
if (result.errorCode !== 0) throw new Error(`SAP CDC error: ${result.errorMessage}`);
return result;
} catch (error) {
console.error('Error getting SAP CDC account info:', error);
return null;
}
}
async getJWT(uid: string, fields: string = 'data.CardNumber'): Promise<string | null> {
try {
const formData = new URLSearchParams();
formData.append('apiKey', this.config.apiKey);
formData.append('userKey', this.config.userKey);
formData.append('secret', this.config.secret);
formData.append('targetUID', uid);
formData.append('fields', fields);
const gigyaApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_API_BASE_URL || 'https://accounts.gigya.com';
const response = await fetch(`${gigyaApiBaseUrl}/accounts.getJWT`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
});
const result = await response.json();
if (result.errorCode !== 0) throw new Error(`SAP CDC error: ${result.errorMessage}`);
return result.id_token;
} catch (error) {
console.error('Error getting SAP CDC JWT:', error);
return null;
}
}
async setAccountInfo(uid: string, data: any): Promise<boolean> {
try {
const formData = new URLSearchParams();
formData.append('apiKey', this.config.apiKey);
formData.append('userKey', this.config.userKey);
formData.append('secret', this.config.secret);
formData.append('UID', uid);
formData.append('data', JSON.stringify(data));
const gigyaApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_API_BASE_URL || 'https://accounts.gigya.com';
const response = await fetch(`${gigyaApiBaseUrl}/accounts.setAccountInfo`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
});
const result = await response.json();
if (result.errorCode !== 0) throw new Error(`SAP CDC error: ${result.errorMessage}`);
return true;
} catch (error) {
console.error('Error setting SAP CDC account info:', error);
return false;
}
}
}
// Singleton instance
let gigyaServiceInstance: GigyaService | null = null;
export function getGigyaService(): GigyaService {
if (!gigyaServiceInstance) {
gigyaServiceInstance = new GigyaService({
apiKey: process.env.NEXT_PUBLIC_GIGYA_API_KEY || '',
userKey: process.env.GIGYA_USER_KEY || '',
secret: process.env.GIGYA_SECRET || '',
});
}
return gigyaServiceInstance;
}
// Client-side logout helper
export async function logoutFromGigya(): Promise<void> {
return new Promise<void>((resolve) => {
try {
const gigyaWindow = window as any;
if (typeof window !== 'undefined' && gigyaWindow?.gigya?.accounts?.logout) {
gigyaWindow.gigya.accounts.logout({
callback: () => {
console.log('SAP CDC logout successful');
resolve();
},
});
} else {
resolve();
}
} catch (error) {
console.error('Error during SAP CDC logout:', error);
resolve();
}
});
}
export default GigyaService;
Step 5: Create Custom NextAuth Provider
Create src/pages/api/auth/[...nextauth].ts:
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getGigyaService } from '@/lib/gigya/gigya-service';
import * as jose from 'jose';
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
// Custom SAP CDC Provider for Sitecore
const GigyaProvider = {
id: 'gigya',
name: 'SAP Customer Data Cloud',
type: 'credentials' as const,
credentials: {
id_token: { label: 'ID Token', type: 'text' },
},
async verifyIdToken(idToken: string) {
try {
// Fetch SAP CDC public key
const gigyaUsApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_US_API_BASE_URL || 'https://accounts.us1.gigya.com';
const pubKeyResp = await fetch(
`${gigyaUsApiBaseUrl}/accounts.getJWTPublicKey?apiKey=${process.env.NEXT_PUBLIC_GIGYA_API_KEY}&format=json`
);
const pubKeyData = await pubKeyResp.json();
if (pubKeyData.errorCode && pubKeyData.errorCode !== 0) {
throw new Error('Failed to fetch SAP CDC public key');
}
const { n, e } = pubKeyData;
if (!n || !e) throw new Error('Public key data is incomplete');
// Verify JWT
const jwk = { kty: 'RSA', n, e };
const key = await jose.importJWK(jwk, 'RS256');
const { payload } = await jose.jwtVerify(idToken, key);
return payload;
} catch (error) {
console.error('SAP CDC JWT verification failed:', error);
throw error;
}
},
async authorize(credentials: any) {
try {
if (!credentials?.id_token) {
throw new Error('No ID token provided');
}
const payload = await this.verifyIdToken(credentials.id_token);
const uid = String(payload.sub || payload.uid || '');
if (!uid) throw new Error('Invalid user ID in token');
// Get user info from SAP CDC
const gigyaService = getGigyaService();
const accountInfo = await gigyaService.getAccountInfo(uid);
if (!accountInfo) throw new Error('Failed to get SAP CDC account info');
// Get or generate user identifier (adjust for your Sitecore setup)
let cardNumber = accountInfo.data?.CardNumber || '';
if (!cardNumber) {
cardNumber = `TEMP_${uid.substring(0, 8).toUpperCase()}`;
await gigyaService.setAccountInfo(uid, { CardNumber: cardNumber });
}
return {
id: uid,
email: accountInfo.profile?.email || '',
name: accountInfo.profile?.firstName || '',
gigyaProfile: payload,
cardNumber: cardNumber,
};
} catch (error) {
console.error('SAP CDC authorization failed:', error);
return null;
}
},
};
// NextAuth configuration for Sitecore
export const authOptions: NextAuthOptions = {
providers: [GigyaProvider as any],
session: {
strategy: 'jwt',
maxAge: 30 * 60, // 30 minutes
},
pages: {
signIn: '/login',
},
callbacks: {
async session({ session, token }) {
if (token) {
session.gigyaProfile = token.gigyaProfile as Record<string, unknown>;
session.cardNumber = token.cardNumber as string;
session.gigyaToken = token.gigyaToken as string;
}
return session;
},
async jwt({ token, user }) {
const isDuringSignin = !!user;
if (isDuringSignin && user && 'gigyaProfile' in user) {
token.gigyaProfile = (user as any).gigyaProfile;
token.cardNumber = (user as any).cardNumber;
// Generate SAP CDC JWT for API calls
const gigyaService = getGigyaService();
const uid = String(token.gigyaProfile?.uid || user.id || '');
if (uid) {
try {
const gigyaToken = await gigyaService.getJWT(uid);
if (gigyaToken) {
token.gigyaToken = gigyaToken;
}
} catch (error) {
console.error('Error getting SAP CDC JWT:', error);
}
}
}
return token;
},
},
events: {
async signOut({ token }) {
console.log('User signed out:', {
hasGigyaProfile: !!token?.gigyaProfile,
cardNumber: token?.cardNumber,
});
},
},
debug: false,
};
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
return await NextAuth(req, res, authOptions);
}
Key Points:
- JWT verification uses SAP CDC's public key (RSA256)
- Session stores minimal data in encrypted HttpOnly cookies
- Works with Sitecore's headless architecture
Step 6: Create Login Page
Create src/pages/login.tsx:
/* eslint-disable @typescript-eslint/no-explicit-any */
import { signIn } from 'next-auth/react';
import { useCallback, useEffect, useState } from 'react';
declare global {
interface Window {
gigya: any;
}
}
const GIGYA_API_KEY = process.env.NEXT_PUBLIC_GIGYA_API_KEY || '';
const GIGYA_SCREENSET = process.env.NEXT_PUBLIC_GIGYA_SCREENSET || 'Default-RegistrationLogin';
const GIGYA_START_SCREEN = process.env.NEXT_PUBLIC_GIGYA_START_SCREEN || 'gigya-login-screen';
function loadGigyaScript() {
return new Promise<void>((resolve, reject) => {
if ((window as any).gigya) return resolve();
const script = document.createElement('script');
const gigyaCdnBaseUrl = process.env.NEXT_PUBLIC_GIGYA_CDN_BASE_URL || 'https://cdns.us1.gigya.com';
script.src = `${gigyaCdnBaseUrl}/js/gigya.js?apiKey=${GIGYA_API_KEY}`;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load SAP CDC SDK'));
document.body.appendChild(script);
});
}
function verifyGigyaSession(): Promise<boolean> {
return new Promise((resolve) => {
const w = window as any;
if (!w.gigya?.accounts?.getAccountInfo) return resolve(false);
w.gigya.accounts.getAccountInfo({
callback: (res: any) => resolve(res.errorCode === 0),
});
});
}
export default function LoginPage() {
const [sdkReady, setSdkReady] = useState(false);
const [error, setError] = useState('');
const callbackUrl =
typeof window !== 'undefined'
? new URLSearchParams(window.location.search).get('callbackUrl')
: null;
const establishAppSession = useCallback(async (idToken: string) => {
try {
const result = await signIn('gigya', {
id_token: idToken,
redirect: false,
});
return result?.ok ? { ok: true } : { ok: false, error: result?.error };
} catch (err: any) {
return { ok: false, error: err.message };
}
}, []);
const handleOnLogin = useCallback(() => {
const w = window as any;
w.gigya.accounts.getJWT({
fields: 'profile,email,emails,data',
expiration: 600,
callback: async (jwtRes: any) => {
if (jwtRes.errorCode === 0 && jwtRes.id_token) {
const result = await establishAppSession(jwtRes.id_token);
if (result.ok) {
window.location.href = callbackUrl || '/my-account'; // Adjust redirect URL
} else {
setError('Session establishment failed: ' + (result.error || 'Unknown error'));
}
} else {
setError('JWT generation failed: ' + (jwtRes?.errorMessage || jwtRes?.errorCode));
}
},
});
}, [establishAppSession, callbackUrl]);
const showScreenSet = useCallback(() => {
const w = window as any;
if (!w.gigya?.accounts?.showScreenSet) return;
w.gigya.accounts.addEventHandlers({
onLogin: handleOnLogin,
});
w.gigya.accounts.showScreenSet({
screenSet: GIGYA_SCREENSET,
startScreen: GIGYA_START_SCREEN,
onError: (e: any) => setError('Screen-Set error: ' + (e?.errorMessage || e?.errorCode)),
});
}, [handleOnLogin]);
useEffect(() => {
let mounted = true;
loadGigyaScript()
.then(async () => {
if (!mounted) return;
setSdkReady(true);
const hasSession = await verifyGigyaSession();
if (hasSession) {
const w = window as any;
w.gigya?.accounts?.getJWT({
fields: 'profile,email,emails,data',
expiration: 600,
callback: async (jwtRes: any) => {
if (jwtRes.errorCode === 0 && jwtRes.id_token) {
const result = await establishAppSession(jwtRes.id_token);
if (result.ok) {
window.location.href = callbackUrl || '/my-account';
}
}
},
});
return;
}
showScreenSet();
})
.catch((e) => mounted && setError(String(e?.message || e)));
return () => {
mounted = false;
};
}, [establishAppSession, callbackUrl, showScreenSet]);
return (
<div>
<h1>Sign In</h1>
{!sdkReady && <div>Loading...</div>}
{error && <div>{error}</div>}
</div>
);
}
export async function getServerSideProps() {
return { props: {} };
}
Sitecore Note: The screen-set displays your branded UI - customize it in SAP CDC Console to match your Sitecore site design.
Step 7: Load SAP CDC SDK Globally
Update your src/Layout.tsx or src/pages/_app.tsx:
import Script from 'next/script';
// In your Layout component
<Script
src={`${
process.env.NEXT_PUBLIC_GIGYA_CDN_BASE_URL || ''
}/js/gigya.js?apiKey=${process.env.NEXT_PUBLIC_GIGYA_API_KEY}`}
strategy="afterInteractive"
/>
This loads the SAP CDC SDK globally for SSO and persistent sessions across your Sitecore site.
Step 8: Add Session Timeout
Create src/components/SessionTimeoutChecker.tsx:
import { useSession, signOut } from 'next-auth/react';
import { useEffect } from 'react';
import { logoutFromGigya } from '@/lib/gigya/gigya-service';
export function SessionTimeoutChecker() {
const { data: session, status } = useSession();
useEffect(() => {
if (status !== 'authenticated' || !session) return;
const timeoutMinutes = Number(process.env.NEXT_PUBLIC_SESSION_TIMEOUT_MINUTES) || 30;
const timeoutMs = timeoutMinutes * 60 * 1000;
let lastActivity = Date.now();
const resetActivity = () => (lastActivity = Date.now());
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
events.forEach((event) => window.addEventListener(event, resetActivity));
const interval = setInterval(async () => {
if (Date.now() - lastActivity >= timeoutMs) {
await logoutFromGigya();
await signOut({ callbackUrl: '/login?reason=timeout', redirect: false });
window.location.href = '/login?reason=timeout';
}
}, 60 * 1000);
return () => {
events.forEach((event) => window.removeEventListener(event, resetActivity));
clearInterval(interval);
};
}, [session, status]);
return null;
}
Add to your _app.tsx:
import { SessionProvider } from 'next-auth/react';
import { SessionTimeoutChecker } from '@/components/SessionTimeoutChecker';
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<SessionTimeoutChecker />
<Component {...pageProps} />
</SessionProvider>
);
}
Step 9: Add Logout
In your Sitecore header component:
import { signOut } from 'next-auth/react';
import { logoutFromGigya } from '@/lib/gigya/gigya-service';
export function LogoutButton() {
const handleSignOut = async () => {
await logoutFromGigya(); // Clear SAP CDC session first
await signOut({ callbackUrl: '/login', redirect: false });
window.location.href = '/login';
};
return <button onClick={handleSignOut}>Sign Out</button>;
}
Step 10: Protect Sitecore Routes
Create src/middleware.ts:
import { getToken } from 'next-auth/jwt';
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
// Define protected Sitecore routes
const protectedRoutes = ['/my-account', '/profile', '/settings'];
const isProtectedRoute = protectedRoutes.some(route => pathname.startsWith(route));
if (isProtectedRoute) {
const token = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!token) {
const url = req.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(url);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api/|_next/|favicon.ico|public/).*)',],
};
Using in Your Sitecore Components
Client-Side (React Components)
import { useSession } from 'next-auth/react';
export function MyAccountComponent() {
const { data: session, status } = useSession();
if (status === 'loading') return <div>Loading...</div>;
if (status === 'unauthenticated') return <div>Please sign in</div>;
return (
<div>
<h1>Welcome, {session?.user?.name}</h1>
<p>User ID: {session?.cardNumber}</p>
<p>Email: {session?.user?.email}</p>
</div>
);
}
Server-Side (Sitecore API Routes)
import { getServerSession } from 'next-auth/next';
import { authOptions } from './auth/[...nextauth]';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Use session.gigyaToken for SAP CDC API calls
// Use session.cardNumber for Sitecore personalization
return res.status(200).json({ message: 'Protected data', user: session.user });
}
Sitecore Page Props
import { getServerSession } from 'next-auth/next';
import { authOptions } from './api/auth/[...nextauth]';
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
if (!session) {
return {
redirect: { destination: '/login', permanent: false },
};
}
return {
props: {
session,
// Pass to Sitecore components
},
};
}
Common Issues and Fixes
"Failed to load SAP CDC SDK"
- Cause: Incorrect CDN URL or API key
- Fix: Check
NEXT_PUBLIC_GIGYA_CDN_BASE_URL matches your data center (us1/eu1/au1)
- Fix: Verify API key is correct in SAP CDC Console
"JWT verification failed"
- Cause: Public key fetch failure or token expiration
- Fix: Ensure
NEXT_PUBLIC_GIGYA_API_KEY is correct
- Fix: Verify data center URLs match your SAP CDC account region
- Fix: Check token hasn't expired (default: 10 minutes)
"Session not persisting"
- Cause:
NEXTAUTH_URL mismatch or missing secret
- Fix: Verify
NEXTAUTH_URL matches your domain exactly
- Fix: Ensure
NEXTAUTH_SECRET is set and consistent across instances
- Fix: Check browser cookies are enabled
"Screen-set not displaying"
- Cause: SDK not loaded or incorrect screen-set name
- Fix: Confirm screen-set name in SAP CDC Console matches
NEXT_PUBLIC_GIGYA_SCREENSET
- Fix: Check browser console for JavaScript errors
- Fix: Verify SDK loaded before calling
showScreenSet()
"Race condition: gigya is not defined"
- Cause: Using SDK before it's loaded
- Fix: Always use
loadGigyaScript() Promise before accessing window.gigya
- Fix: Check SDK loading status before making API calls
Security Best Practices
Essential Security Checklist
✅ Environment Variables
- Add
.env.local to .gitignore
- Use different secrets for dev/staging/production
- Store production secrets in hosting platform (Vercel, Azure, etc.)
✅ JWT Security
- Tokens stored in HttpOnly cookies (XSS protection)
- Short expiration times (30 minutes recommended)
- Always verify tokens server-side
✅ Session Management
- Implement session timeout (default: 30 minutes)
- Clear both SAP CDC and NextAuth sessions on logout
- Validate sessions on every protected request
✅ API Security
// Always verify session server-side
const session = await getServerSession(req, res, authOptions);
if (!session) return res.status(401).json({ error: 'Unauthorized' });
✅ CORS Configuration
- Whitelist your domains in SAP CDC Console → Security → Trusted Sites
- Never use wildcard (
*) in production
✅ HTTPS
- Always use HTTPS in production
- Secure cookies require HTTPS
Production Deployment
Sitecore XM Cloud Deployment
- Add environment variables in XM Cloud portal or CI/CD pipeline
- Configure SAP CDC trusted sites with production URLs
- Update NEXTAUTH_URL to production domain
- Test authentication thoroughly before going live
- Enable HTTPS and secure cookies
- Monitor authentication logs and errors
Vercel Deployment
vercel env add NEXTAUTH_SECRET production
vercel env add GIGYA_USER_KEY production
vercel env add GIGYA_SECRET production
# Add all other secrets
Resources
Conclusion
Implementing SAP Customer Data Cloud authentication in Sitecore XM Cloud Next.js applications requires careful attention to JWT verification, session management, and SDK initialization. The challenges we faced—from dynamic public key fetching to session synchronization—are common in enterprise authentication implementations.
By following this guide and addressing the challenges we documented, you can build a production-ready authentication system that:
✅ Securely authenticates users with JWT-based tokens
✅ Integrates with Sitecore's headless architecture
✅ Maintains session state across page reloads and navigation
✅ Protects routes with middleware-based authentication
✅ Provides foundation for personalization with Sitecore Personalize
Remember: Always follow security best practices, keep dependencies updated, and test thoroughly in staging before production deployment. The authentication layer is critical—take time to get it right.
Happy coding! 🚀