Imagine your E-Commerce platform uses a Next.js/WordPress monolith that seamlessly routes users to two React SPAs (seller and customer) via JWTs and OAuth 2.0. To harden this flow against XSS and CSRF, we have to shift all tokens into HTTP-only cookies, implement rotating refresh tokens, and provide a robust logout/expiry strategy. You’ll have a single, secure session across all three applications by the end.
Token Flow Overview
- Browser →
/api/login
: credentials submitted /api/login
→ Browser: setsaccess_token
&refresh_token
cookies- Browser →
/api/graphql
: requests include cookies /api/graphql
→ Browser: returns user context- Role-Based Redirects: Next.js sends user to Seller or Customer SPA
Prerequisites
- Node.js ≥14
- A Next.js project with WordPress authentication (OAuth 2.0)
- React SPAs for Seller and Customer, served under the same domain
- A token store (DB or in-memory cache) for refresh tokens
1. Issue Access & Refresh Tokens on Login
In pages/api/login.js
, validate users via WordPress, then sign and set two cookies:
// pages/api/login.js
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
import { verifyWordPressUser } from '../../lib/wpAuth';
import { saveRefreshToken } from '../../lib/tokenStore';
const ACCESS_EXPIRES = '15m';
const REFRESH_EXPIRES = 7 * 24 * 3600; // 7 days in seconds
const { JWT_SECRET, JWT_REFRESH_SECRET } = process.env;
export default async function handler(req, res) {
if (req.method !== 'POST') return res.status(405).end();
const { username, password } = req.body;
const user = await verifyWordPressUser(username, password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const accessToken = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: ACCESS_EXPIRES });
const refreshToken = jwt.sign({ id: user.id }, JWT_REFRESH_SECRET, { expiresIn: REFRESH_EXPIRES });
await saveRefreshToken(user.id, refreshToken);
res.setHeader('Set-Cookie', [
cookie.serialize('access_token', accessToken, {
httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 15*60
}),
cookie.serialize('refresh_token', refreshToken, {
httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: REFRESH_EXPIRES
}),
]);
res.status(200).json({ message: 'Logged in', role: user.role });
}
This issues a short-lived access_token cookie and a longer-lived refresh_token cookie, both inaccessible to JavaScript.
2. Verify Tokens in GraphQL Middleware
In pages/api/graphql.js
, parse cookies, verify tokens, and inject context.user
:
// pages/api/graphql.js
import { ApolloServer } from 'apollo-server-micro';
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
import typeDefs from '../../graphql/schema';
import resolvers from '../../graphql/resolvers';
const { JWT_SECRET } = process.env;
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const { access_token } = cookie.parse(req.headers.cookie || '');
try {
const payload = jwt.verify(access_token, JWT_SECRET);
return { user: { id: payload.id, role: payload.role } };
} catch {
return { user: null };
}
},
});
export const config = { api: { bodyParser: false } };
export default server.createHandler({ path: '/api/graphql' });
Your resolvers can now guard data based on context.user.role
for both SPAs and Next.js pages.
3. React SPAs: Unified Auth Context
In both seller and customer apps, wrap your app in AuthProvider
:
// src/AuthContext.jsx
import React, { createContext, useState, useEffect } from 'react';
import { fetchWithRefresh } from './fetchWithRefresh';
export const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchWithRefresh('/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: '{ me { id role } }' }),
})
.then(res => res.json())
.then(({ data }) => setUser(data.me))
.catch(() => setUser(null));
}, []);
const login = async creds => {
const res = await fetch('/api/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(creds),
});
const { role } = await res.json();
setUser({ role });
window.location.href = role === 'seller' ? '/seller-dashboard' : '/customer-home';
};
const logout = async () => {
await fetch('/api/logout', { method: 'POST', credentials: 'include' });
setUser(null);
window.location.href = '/';
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Use credentials: 'include'
everywhere so cookies travel automatically.
4. Refresh Tokens: Rotate & Retry
Refresh Endpoint (pages/api/refresh.js
)
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
import { getRefreshToken, saveRefreshToken, revokeRefreshToken } from '../../lib/tokenStore';
const { JWT_SECRET, JWT_REFRESH_SECRET } = process.env;
const ACCESS_EXPIRES = '15m';
const REFRESH_EXPIRES = 7 * 24 * 3600;
export default async function handler(req, res) {
const { refresh_token } = cookie.parse(req.headers.cookie || '');
if (!refresh_token) return res.status(401).end();
let payload;
try { payload = jwt.verify(refresh_token, JWT_REFRESH_SECRET); }
catch { return res.status(401).end(); }
const stored = await getRefreshToken(payload.id);
if (stored !== refresh_token) {
await revokeRefreshToken(payload.id);
return res.status(401).end();
}
await revokeRefreshToken(payload.id);
const newRefresh = jwt.sign({ id: payload.id }, JWT_REFRESH_SECRET, { expiresIn: REFRESH_EXPIRES });
const newAccess = jwt.sign({ id: payload.id, role: payload.role }, JWT_SECRET, { expiresIn: ACCESS_EXPIRES });
await saveRefreshToken(payload.id, newRefresh);
res.setHeader('Set-Cookie', [
cookie.serialize('access_token', newAccess, { httpOnly:true, secure:process.env.NODE_ENV==='production', sameSite:'lax', path:'/', maxAge:15*60 }),
cookie.serialize('refresh_token', newRefresh, { httpOnly:true, secure:process.env.NODE_ENV==='production', sameSite:'lax', path:'/', maxAge:REFRESH_EXPIRES }),
]);
res.status(200).json({ message: 'Tokens refreshed' });
}
Client Retry Logic (src/fetchWithRefresh.js
)
export async function fetchWithRefresh(url, opts = {}) {
opts.credentials = 'include';
opts.headers = { 'Content-Type':'application/json', ...(opts.headers||{}) };
let res = await fetch(url, opts);
if (res.status === 401) {
const refresh = await fetch('/api/refresh', { method:'POST', credentials:'include' });
if (refresh.ok) res = await fetch(url, opts);
else { window.location.href = '/'; throw new Error('Session expired'); }
}
return res;
}
This ensures seamless background rotation and automatic retry of failed requests.
5. Logout & Expiry Flow
Logout Endpoint (pages/api/logout.js
)
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
import { getRefreshToken, revokeRefreshToken } from '../../lib/tokenStore';
const { JWT_REFRESH_SECRET } = process.env;
export default async function handler(req, res) {
if (req.method !== 'POST') return res.status(405).end();
const { refresh_token } = cookie.parse(req.headers.cookie || '');
if (refresh_token) {
try { const payload = jwt.verify(refresh_token, JWT_REFRESH_SECRET); await revokeRefreshToken(payload.id); }
catch {};
}
res.setHeader('Set-Cookie', [
cookie.serialize('access_token', '', { httpOnly:true, secure:process.env.NODE_ENV==='production', sameSite:'lax', path:'/', maxAge:0 }),
cookie.serialize('refresh_token', '', { httpOnly:true, secure:process.env.NODE_ENV==='production', sameSite:'lax', path:'/', maxAge:0 }),
]);
res.status(200).json({ message: 'Logged out' });
}
Client Logout
const logout = async () => {
await fetch('/api/logout', { method:'POST', credentials:'include' });
setUser(null);
window.location.href = '/';
};
Use this on your logout buttons to clear cookies and revoke server-side tokens.
Conclusion
By consolidating access and refresh tokens into HTTP-only cookies, rotating on each use, and providing a clear logout strategy, the E-Commerce platform will benefit from:
- Strong XSS Protection: JavaScript can’t access your tokens
- CSRF Defense: leveraging SameSite and optional double-submit tokens
- Seamless UX: single sign-on across Next.js and both React SPAs
- Robust Session Management: short-lived access, long-lived refresh, and forced logout flows
Leave a Reply