I've googled enough. Almost every topic-related guide on the Internet ends after showing example of storing token into cookies and maybe session refresh example. But for me, the most interesting (and hard) part comes after - when I want to make http-requests to token-protected routes.
In the context of making http-requests in my project, the main agents are:
Desired auth flow:
{ cookies } from 'next/headers'
and sends response back to client;So there is my list of questions:
a) I've noticed that for http-requests from NextJS server side using NextJS fetch() function, credentials: "include"
does not include my previously set cookie. So I guess before every request I need to get token with cookies().get("user_token")?.value
. Is that right or there is a way to make credentials: "include"
work?
b) If (in production) NextJS server and Backend server will run on the same domain, would my NextJS client-side requests automatically include "user_token" cookie?
c) While in dev mode, is there anything I can do in order to set "user_token" cookie both for localhost:3000 (nextJS server) and backend domain? Or I should just manually set it every time while I am developing? (Currently in dev mode "user_token" sets for localhost:3000 and therefore cookie is not sent when making request directly from client-side to backend).
Notes:
I will include some code listings, but context of my question can be understood without looking at them.
login server action (simplified code):
export const loginAction = async (state: unknown, formData: unknown): Promise<LoginFormState> => {
if (!(formData instanceof FormData))
return {
message: "Invalid payload",
ok: false
};
// Validate form fields
const validatedFields = LoginFormSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
// If any form fields are invalid, return early
if (!validatedFields.success) {
console.log('validation error');
return {
errors: validatedFields.error.flatten().fieldErrors,
} as LoginFormState
}
const loginResponse = await loginPost({
...validatedFields.data
});
if (!classicNextApiResponseValidator(loginResponse)) {
const error = await requestErrorFromNextResponse(loginResponse);
return {
message: error.description,
ok: false
}
}
const responseData: T.LoginResponse = await loginResponse.json();
await storeToCookies("user_token", responseData.accessToken);
if (responseData.emailVerified) {
await redirectAction("/dashboard", {});
} else {
await redirectAction("/emailVerification", {});
}
return {
message: "successfully",
ok: true
}
};
storeToCookies Function
"use server";
import { cookies } from 'next/headers';
import { CookiesKey } from './types';
export const storeToCookies = async <T = any>(key: CookiesKey, value: T, options: {} = {}) => {
if (typeof key === "string")
cookies().set(
key,
typeof value === "string" ? value : JSON.stringify(value),
{
httpOnly: true,
secure: false,
sameSite: 'lax',
path: '/',
}
);
};
My attempt to make request to protected route (profile) from nextJS server side (credentials: "include"
does not work, but manually getting cookie and passing it as header works:
export const profileGet = async () => {
return fetch(`${CONFIG.BASE_URL}/profile`, {
method: "GET",
credentials: "include",
headers: DEFAULT_HEADERS,
});
};
My apisauce instance for client-side requests:
const sauce = create({
withCredentials: true,
baseURL: CONFIG.BASE_URL,
headers: {
Accept: 'application/json',
}
});
a) Seems like there is no way to make credentials: "include"
work like intended, but cookies().get("user_token")?.value
is pretty much ok because on server-side it just looks into cookies of incoming request from client-side and gets the cookie; Plus http-only works fine here, so no restrictions.
b) Yes, client-side would automatically include a cookie when sending requests both to server-side and remote BE in production if cookie's domain is configured and Next.js server-side and remote BE share same domain (being different subdomains). Here is example of helper function that I use in order to set cookie's domain property both for dev mode and production mode:
/**
* @returns {string | undefined} Suitable cookie domain option based on the current environment.
*/
export const getCookieDomain = (): string | undefined => {
return process.env.NEXT_PUBLIC_IS_DEPLOYED === 'true' ? CONFIG.DOMAIN : undefined;
};
CONFIG.DOMAIN there is just "your-site.com". Then if Next.js server runs on "your-site.com" and BE runs on "api.your-site.com", everything would be fine.
c) I ended up asking BE team to add support for both cookies auth and "Authorization" header auth types, then (in dev only) I use axios interceptor that manually gets "user_token" from cookies and sets it as "Authorization" header. It is possible even with "http-only" cookie if we're reading cookie through server action. Just make sure this trick is only allowed in dev mode:
/**
* Retrieves the value of a cookie by its key. Be careful when using with HTTP-only cookies, as wont be returned.
*
* @param {CookiesKey} key - The key of the cookie to retrieve.
* @returns {Promise<string | undefined>} - A promise that resolves to the value of the cookie, or undefined if the key is not a string,
* the cookie is HTTP-only and the environment is not development, or the cookie does not exist.
*/
export const getFromCookies = async (key: CookiesKey) => {
if (typeof key !== "string")
return undefined;
if (HTTP_ONLY_COOKIES.has(key) && process.env.NEXT_PUBLIC_IS_DEPLOYED === 'true') // If the cookie is HTTP-only, we should not allow access to it from the client side
return undefined;
return cookies().get(key)?.value;
};