I have a Next.js frontend and an Express backend. My backend handles user authentication, and I use a JWT token for authentication and store it in the Express session. I want to connect this flow with NextAuth for authentication in Next.js, but I am facing issues with managing and persisting the Express session in the browser and passing it back to Express.
Backend Express Login code
export class Login {
public async login(req: Request, res: Response): Promise<Response> {
const { email, password }: Pick<UserInterface, 'email' | 'password' > = req.body;
// Check if user exists with the given email
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
throw new BadRequestError('Invalid email or password');
}
// Compare the provided password with the stored hashed password using bcrypt
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new BadRequestError('Invalid email or password');
}
// Generate a JWT token
const token = jwt.sign(
{ email: user.email, username: user.username, role: user.role }, // Payload
config.JWT_SECRET, // Secret key for signing the token
{ expiresIn: '1h' } // Token expiration time
);
req.session = {jwt: token};
// Return a success message (without sending the token directly in the response body)
return res.status(200).json({
message: 'Login successful', token, role: user.role
});
}
}
Now for each route I have to use this middleware to check for the the validity of the JWT token stored in the session:
export class AuthMiddleware {
public verifyUser(req: Request, _res: Response, next: NextFunction): void {
if (!req.session?.jwt) {
throw new NotAuthorizedError('Token invalid');
}
try {
const payload: AuthPayload = JWT.verify(req.session?.jwt, config.JWT_SECRET) as AuthPayload;
req.currentUser = payload;
} catch (error) {
throw new NotAuthorizedError(`Token invalid. Please login again: ${JSON.stringify(error)}`);
}
next();
}
}
export const authMiddleware: AuthMiddleware = new AuthMiddleware();
example:
export default(app:Application) => {
const routes = () =>{
app.use(BASE_PATH, authMiddleware.verifyUser, unitsRoutes.routes());
};
routes();
};
I am using cookieSession to manage sessions in Express. It works well with Postman.
public Start(): void {
this.securityMiddleware(this.app);
// other middlewares come in here
}
private securityMiddleware(app: Application): void {
app.use(
cookieSession({
name: 'session',
keys: [config.SECRET_COOKIE_KEY_ONE!, config.SECRET_COOKIE_KEY_TWO!],
maxAge: 24 * 7 * 3600000,
// secure: config.NODE_ENV !== 'development'
secure: false,
signed:false,
httpOnly:false,
sameSite:'none',
})
);
app.use(cookieParser()); // Use cookie-parser to access cookies in req.cookie
app.use(hpp());
app.use(helmet());
app.use(
cors({
origin: '*',
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS']
})
);
}
The issue I’m facing is when integrating NextAuth for authentication in a Next.js project using the Credentials provider. My expectation is that once the user is authenticated by the backend and a cookieSession is created, the response should automatically set the req.session cookie on the browser, along with the appropriate headers as defined by the cookieSession middleware on the backend. However, this is not happening. There appears to be a barrier in how NextAuth handles response headers from a remote server. Even after successful authentication and the creation of a cookieSession by the backend, NextAuth generates its own session cookie, overriding / ignoring the backend’s session cookie. As a result, to use the backend’s session cookie, I must manually fetch it from the response headers and set it myself. Unfortunately, this process doesn’t seem to integrate well with NextAuth, as it continues to use its own session mechanism. Here’s the NextAuth configuration with the CredentialsProvider:
import { NextApiRequest, NextApiResponse } from "next";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
export interface AuthPayload {
// userId: string;
email: string;
username: string;
role: 'admin' | 'user';
}
export const options: NextAuthOptionsCallback = (_req: NextApiRequest,res: NextApiResponse) =>{
return {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: {
label: "email:",
type: "text",
placeholder: "Enter your email",
},
password: {
label: "Password:",
type: "password",
placeholder: "Enter password",
},
},
async authorize(credentials) {
const response = await fetch("http://localhost:8081/api/v1/login", {
method: "POST",
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" },
});
const user = await response.json();
// If no error and we have user data, return it
if (response.ok && user) {
const cookies = response.headers.get('set-cookie')
if (cookies) {
// Set the cookie from the backend to the response
res.setHeader('Set-Cookie', cookies);
}
return user;
}
// Return null if user data could not be retrieved
return null;
},
}),
],
// Callbacks to handle JWT and Session data
callbacks: {
async jwt({ token, user }) {
// Save user details (email, username, role) to the JWT token
if (user) {
// Save user details (email, username, role) to the JWT token
// token.email = user.email;
console.log('user is ', user)
token.token = user.token
token.role = user.role;
token.session = user.token
}
return token;
},
// using the role in client component
// async session({ session, token }) {
// console.log('Session callback reached');
// console.log('Token in session callback: ', token); // Check the token structure
// console.log('session is ',session)
// if (session) {
// // session.user.email = token.email;
// // session.user.username = token.username;
// console.log('token is ', token);
// session.session.jwt = token.session
// // session.user.session= token.session
// // session.user.role = token.role;
// }
// return session;
// },
},
}
}
So, everytime i send a request back to express and use credentials as true eg
export const InventoryApi = createApi({
baseQuery: fetchBaseQuery({
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
credentials: "include"
}),
it is the cookieSession by nextAuth that is getting sent to the backend and not the original session generated by express.
Example:
{
'next-auth.csrf-token': '8a1e7c846d2bd62d8b167195c3c949a822aa9fe98b2005b0dde809021115e3dd|00ecab2216e215e08efda994dc4141185a502e43c61a983d227c57e624fbb212',
'next-auth.callback-url': 'http://localhost:3000',
'next-auth.session-token': 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..vnmck6_magHu4k3t.fQpkxzXRA2A5W3YKiIPV9N0ahVfs_KFsM4_2P9_1D2YgcbM7y9zKNvSLiNOhvglh14E4cdet4nKIBSL5gXHKIeHV266hJrpsLwuA9KP6pgTrwiRpR8UeaN5KtKeRkfoa45H9wv7EnXewifZ1Y1vvOSAGDwu9owOMusa5RT6CtulLpXNsr6BCLJ1U-fHCQUP5YEowhozciqKaoXgkVfhz0PZNd7hoqDaBM8x8uMhHX-JiXgSN_j78fH97_H255SRU2j0dut9Z194Wiv76U5IFbXXP4ZRBX6D3N5FArvmlNaP99hiIoQJxNL0ve1kFUNzOQcEhCaAj3SkT3rU1qncogVy8l9E1wipXfac9binjhCkG08PPVwpquLWe97eNHDYMbQI_LcIflDDnhKYcldZbkwEyLng3Jw5lV8A1UfP9vPRkz37wILJp-bMmigu3zQYcJw8GXSTGY36PMOAd42zd-4B5zHq5oz0okCukCPNAXFK915-HMvZsi0Iq5TbFgjAWNHHwRvSKT927ZKDMUsldHTxTy4M_cI6h-VZl5GFF6ZSC2x6m6LdzxqPylb8rTrfCIv4sQr-M9VD_3Q7Fc05ikibxuNOc2nWz3QyelTOtg7G_gnPwzYyXbKtBZGxePxIsR-K93-WCbEd2vuA53MxX35FEXfRjtGaqVqlsQShCCSfpljRbtRpFJOa4Bk6EGqMbTcQHgV8oaFHW4mcgXR6TOX5kmdfqpMQyRV_QD90q6rou2iSIU8-0PuWxo_N4P1xSGTWUYr2I1INX46UlhW7wWj8p7AAMs3uxN03ZcfV7kbQi2fWKQUH-adDbAzykjWVmwvuDnVGZOMndG4_1wUIo.WApg0cuxigjTu7gaM6RY7g'
}
My understanding is that express cannot work with ‘next-auth.session-token’ since it did not generate it . so am not able to verify the user since that sessioncookie canot be understood by express. Again, Even if I manually set the session header or try to sync the session from NextAuth to my Express app, NextAuth’s session token is still not usable directly by Express. The reason is that NextAuth’s session is independent, and the token it uses is encrypted and signed with NextAuth’s secret, which my Express app doesn’t have
How can I persist the session created by my Express backend on the browser and ensure that it’s sent back to the backend in subsequent requests,
I understand that one option is to manually fetch the token on the client side and embed it in each request as a payload. However, I don’t want to take this approach, as it defeats the purpose of securely storing the session on the browser. The goal is for the session to be automatically managed and sent with each request, without manually handling the token in every request.
I’ve researched and tried several approaches, including manipulating cookies, and configuring both NextAuth and the backend for cross-domain authentication on my CORS. However, it became increasingly difficult to get everything working as expected. From my findings, it seems NextAuth is primarily made for single-domain applications, and when it comes to handling cross-domain scenarios like mine, things become tricky. That’s why I’m turning to StackOverflow for help, hoping to find a solution that properly integrates the backend session with NextAuth on the frontend or a way forward or maybe am doing it wrong.
I am also aware this kind of question but not detailed was asked here before but not answered. How Can I Send the NextAuth Session Token to Express.js Server for Authentication