Update cookies after fetch
Answered
Evanion posted this in #help-forum
EvanionOP
I have a project that consist of two parts a Next.js (14) frontend and a separate backend.
When the user authenticates, the backend returns an object containing both the
I need to store these in the users cookies, and then include them in each request to the backend in the
Since the
Once the new tokens are returned, I need to update the stored cookies, and then return the new
But when I try to write to the store, I get an error saying
I'm still in the same
I then use the
I'm using both
get-token.ts:
When the user authenticates, the backend returns an object containing both the
accessToken and refreshTokenI need to store these in the users cookies, and then include them in each request to the backend in the
authorization header. If I could use cookies on the backend, I could have just done credentials: "include". But the backend is serving other platforms as well, and we want a uniform auth flow on the backend.Since the
accessToken is short lived (TTL 1h) I need to repeatedly refresh the tokens. So I have a a server action (getTokens) that calls cookies(), checks that the accessToken is valid for at least 1 min, and if not, it performs a fetch to the backends [POST] /authorization/refresh-tokens endpoint.Once the new tokens are returned, I need to update the stored cookies, and then return the new
accessToken.But when I try to write to the store, I get an error saying
Cookies can only be modified in a Server Action or Route Handler.I'm still in the same
getTokens function, I'm even using the same cookieStore instance that I was using in the beginning of the function, I have also tried just using a new cookies() call for setting each new token.I then use the
getToken action in my other server actions that requires authentication to get the token and include it in the fetch request.I'm using both
'use server'; and server-only So if the function wasn't run server side, I would expect other errors.get-token.ts:
'use server';
import { cookies } from 'next/headers';
import 'server-only';
import { Tokens } from '../types';
import { TokenPayload } from '../types/token-payloads';
const SECOND_IN_MS = 1000;
const MINUTE_IN_MS = 60 * SECOND;
/**
* Returns the access token to use for API requests against the backend
* If the token is expired, it will be refreshed
* If it's unable to refresh the token, it will throw an error
* @returns accessToken
*/
export async function getToken(): Promise<string | undefined> {
const cookieStore = cookies();
const token = cookieStore.get('token');
const refreshToken = cookieStore.get('refreshToken');
if (token?.value) {
// check if token is expiring
const {exp}: TokenPayload = JSON.parse(atob(token.value.split('.')[1]));
const now = Date.now();
// if token have more than 1 minute left, return it
if (now - exp > MINUTE_IN_MS) return token.value;
}
// if no refresh token, we can't refresh the token
if (!refreshToken?.value) throw new Error('NO_TOKEN');
const result = await fetch(
`${process.env.API_URL}/authentication/refresh-tokens`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken: refreshToken.value }),
}
);
if (!result.ok) throw new Error('NO_TOKEN');
const tokens: Tokens = await result.json();
// if unable to refresh token, return undefined
if (!tokens) throw new Error('NO_TOKEN');
// set the new tokens
// Have also tried `cookies().set`
cookieStore.set('token', tokens.accessToken, {
httpOnly: true,
maxAge: parseInt(process.env.JWT_ACCESS_TOKEN_TTL!, 10),
});
cookieStore.set('refreshToken', tokens.refreshToken, {
httpOnly: true,
maxAge: parseInt(process.env.JWT_REFRESH_TOKEN_TTL!, 10),
});
cookieStore.set('user', JSON.stringify(tokens.user), {
httpOnly: false,
});
return tokens.accessToken;
}Answered by Evanion
ok, well then I have a path forward, writing a middleware that checks and refreshes the token before next starts rendering the page 👠Thanks
33 Replies
EvanionOP
I have tried it in a number of places, but for instance in another server action like
that then get's called in
getCurrentUser'use server';
import { plainToInstance } from 'class-transformer';
import { User } from '@codesmith/entities';
import 'server-only';
import { getToken } from './get-token';
export async function getCurrentUser() {
const token = await getToken();
if (!token) return;
const user = await fetch(`${process.env.API_URL}/users/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
}).then((res) => res.json());
return plainToInstance(User, user);
}that then get's called in
/app/profile/page.tsx:import { getCurrentUser } from '@codesmith/actions/server';
export default async function ProfilePage() {
const user = await getCurrentUser();
console.log('user', user);
return (
<div>
<div className="wrapper">
<pre>{JSON.stringify(user, null, 2)}</pre>
</div>
</div>
);
}the getCurrentUser is called inside a server component
therefore getToken is run inside a server component
EvanionOP
sure, but you're suppose to be able to do that
Not in server component
because by the time it reaches there, page.js already starts returning Response to the client as part of the Suspense feature
and you can't change the SetCookie if its already sending the response :/
"use server" file that is called inside a server component before the return part is not a Server Action, its just a regular function. Thats why the error is confusing
EvanionOP
ok, so how can I solve the basic issue of including a token when fetching the current user, and if that token needs updating before doing the request, perform the update before that?
Would I need to do the update in a middleware, and then just read the token when performing the request?
Would I need to do the update in a middleware, and then just read the token when performing the request?
export default async function ProfilePage(){
const token = await getToken() // not a server action
return (
<form action={getToken}> // server action
<button>submit</button>
</form>
)
}Would I need to do the update in a middleware, and then just read the token when performing the request?
Yes, its one of the solution right now
or update the cookie before redirecting to a page
EvanionOP
well since the user can land directly on the page, the only place that I know to do that would be in a middleware
yep
cookies are stil sent to the Next.js server for every fetch/get request so that shouldnt be a problem
EvanionOP
ok, well then I have a path forward, writing a middleware that checks and refreshes the token before next starts rendering the page 👠Thanks
Answer
Yes im learning how to do that too :))
EvanionOP
yea the docs around async actions/functions/use server, and cookies are a bit slim.. they are basically the same thing, but not really.. and you can basically use the same code in both
yep, make sure you still protect "server action" with auth though
EvanionOP
yea in this case I do that in the parent
layout.tsxmy way of understanding it is
"use server" is a client-to-server boundaries.
They create instant endpoint for every function that is created in that file
"use server" is a client-to-server boundaries.
They create instant endpoint for every function that is created in that file
that is not what im talking about
EvanionOP
oh, yea I get waht you mean
well actions that you put on a form are basically transformed in to post routes, so you need to do the check in them
instant endpoint means that that function is open to the public, like a regular route handler.
so even if you only use it in page.js, doesnt mean that its function is private
so even if you only use it in page.js, doesnt mean that its function is private
im saying that you dont know what the bundler does to that function. Worst case scenario it opens a public route
treat it like a regular POST route
EvanionOP
yea it basically just mounts it as a route handler on an API route
EvanionOP
so just like any route handler, you need to check the auth in the function.
in my case, the cookies are basically JWT tokens, so I also have a
The backend also performs it's own validation of the token
in my case, the cookies are basically JWT tokens, so I also have a
auth() function that validates the accessToken and checks the exp claim on the token (token expiration), and if no accessToken is included, or it's expired (the cookie also has a TTL) the user isn't authenticated.The backend also performs it's own validation of the token
EvanionOP
Well middleware was a Bust .. it's only available in
edge runtimes, and the default in next is nodejs runtime.