The Problem
I recently encountered a challenging authentication issue when working with Next.js Server Actions connecting to a NestJS backend API.
When making authenticated requests from client components everything works fine, but when using Server Actions (with the "use server"
directive), the authentication token wasn't being passed to my external API, resulting in 401 Unauthorized errors.
Context
My architecture:
- Frontend: Next.js app with next-auth for authentication
- Backend: NestJS API requiring JWT token authentication
- Authentication: JWT tokens stored in next-auth session
Client-side Code
// axios.ts - My API handler for HTTP requests
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { getSession } from "next-auth/react";
const axiosInstance: AxiosInstance = axios.create({
baseURL: "http://localhost:3334/api",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// This works fine for client-side requests
axiosInstance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session?.accessToken) {
request.headers.Authorization = `Bearer ${session.accessToken}`;
}
return request;
});
export const apiHandler = async (
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
url: string,
body?: any,
params?: any,
) => {
const config: AxiosRequestConfig = {
method,
url,
data: body,
params: {
...params,
where: params?.where ? JSON.stringify(params.where) : undefined,
orderBy: method === "GET" ? params?.orderBy || "createdAt:desc" : undefined,
},
};
try {
const res = await axiosInstance(config);
return {
status: res.status,
data: res.data,
};
} catch (error: any) {
// Error handling
console.log(`error`, error);
throw {
status: error.response?.status || 500,
data: { message: error?.response?.data?.message || "An error occurred" },
};
}
};
export const setAuthToken = (token?: string) => {
if (token) {
axiosInstance.defaults.headersmon["Authorization"] = `Bearer ${token}`;
} else {
delete axiosInstance.defaults.headersmon["Authorization"];
}
};
Server Action
"use server";
import { apiHandler } from "@/lib/axios";
export const countMissions = async (params) => {
const res = await apiHandler(
"GET",
`/mission/count/specific`,
undefined,
params,
);
return res?.data || 0;
};
Usage in Components
// Client component
'use client';
import { countMissions } from "@/actions/mission";
import { useSession } from "next-auth/react";
export function MissionsComponent() {
const { data: session } = useSession();
// This works fine until we call countMissions
useEffect(() => {
if (session?.accessToken) {
// Set token for client-side requests
setAuthToken(session.accessToken);
// But this fails with 401 because the server action can't access the token
countMissions({ where: { /* params */ } }).then(data => {
// Process data
});
}
}, [session]);
return <div>Missions content</div>;
}
Server Logs
[Nest] 105854 - 03/21/2025, 6:29:28 PM WARN [HTTP] GET http://localhost:3334/api/mission/count/specific?where=%7B%22talents%22:%7B%22some%22:%7B%22talentId%22:%22cm7z762p70002svpppibalqr5%22%7D%7D%7D&orderBy=createdAt:desc 401, data object : {"date":"2025-03-21T18:29:28.991Z","method":"GET","protocol":"http","path":"/api/mission/count/specific","authorization":"","duration":3,"status":401} 3ms
The key issue is visible in the logs: "authorization":""
- the token is missing when the request is made from the server action.
Solutions I'm Considering
Pass Token as Parameter: Update all server actions to accept a token parameter
export const countMissions = async (params, token) => { // Pass token in headers };
Token Store: Create a shared token store accessible to server actions
// Client-side: set token setGlobalToken(session.accessToken); // Server-side: use token const token = getGlobalToken();
Remove "use server": Convert server actions to client-side functions (works but loses server benefits)
Use getServerSession: Retrieve session directly in server actions (if next-auth is properly configured)
import { getServerSession } from "next-auth/next"; export async function serverAction() { const session = await getServerSession(); // Use session.accessToken }
Questions
- What's the recommended way to handle this authentication pattern in Next.js apps with external APIs?
- Are there built-in solutions in Next.js for passing auth tokens to server actions?
- Has anyone implemented a clean solution that doesn't require refactoring every server action?
- What are the security implications of the different approaches?
Looking forward to hearing how others are solving this common challenge!