-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update Session on Server Side #8254
Comments
I think there isnt a way to update the session server side yet. I think #6642 is related |
I have similar issue as the above! Can't update session in a server component as 'use session' which has the 'update' method only works on client side. Any idea to go around this would be appreciated. |
As far as I know, there is absolutely no way to update the session in a server component with Next.js app router because of the way React does streaming rendering of server components. Since you're wanting to update the session on the server, ideally you would need to set the cookies or headers. However, you just can't set cookie or headers during rendering of server components which is why the Next.js docs only say you can read them. From what I've gathered, you can only check if the session is valid in a server component, but not update it. However, if you're wanting to update the cookies (in order to update the session), you would either have to do this on the client or in a middleware. But since you want a more server-side approach, you can do this with middleware.
In the middleware, you can intercept those request tokens, validate those tokens (by a server-side function or API call), and finally set the response tokens and thereby updating the session. |
@rinvii Thank you for the detailed response. I was thinking about implementing NextAuth with my custom backend but I dont know how to properly handle token refresh in sync with the NextAuth session expiration. I could just make my backend return tokens with 1 year expiration time but as the default session expiration for NextAuth is 1 month I think, I would like the accessToken from my backend stored inside the next-auth session to refresh when the next auth session is updated. As you say I should do it in the middleware, but I dont know how to properly do that. Do you have some sample code to check and study? |
Hmmm, can it be a little smarter? so that if the client updates the access token by update method then the client can call the backend API to update the backend access token. is that possible? |
You can probably do something like this: export const config = {
matcher: "/:path*",
};
export const middleware: NextMiddleware = async (request: NextRequest) => {
if (request.nextUrl.pathname.startsWith("/protected")) {
const cookiesList = request.cookies.getAll();
const sessionCookie = env.NEXTAUTH_URL?.startsWith("https://")
? "__Secure-next-auth.session-token"
: "next-auth.session-token";
// no session token present, remove all next-auth cookies and redirect to sign-in
if (!cookiesList.some((cookie) => cookie.name.includes(sessionCookie))) {
const response = NextResponse.redirect(new URL("/sign-in", request.url));
request.cookies.getAll().forEach((cookie) => {
if (cookie.name.includes("next-auth"))
response.cookies.delete(cookie.name);
});
return response;
}
// session token present, check if it's valid
const session = await fetch(`${ env.NEXTAUTH_URL }/api/auth/session`, {
headers: {
"content-type": "application/json",
cookie: request.cookies.toString(),
},
} satisfies RequestInit);
const json = await session.json();
const data = Object.keys(json).length > 0 ? json : null;
// session token is invalid, remove all next-auth cookies and redirect to sign-in
if (!session.ok || !data?.user) {
const response = NextResponse.redirect(new URL("/sign-in", request.url));
request.cookies.getAll().forEach((cookie) => {
if (cookie.name.includes("next-auth"))
response.cookies.delete(cookie.name);
});
return response;
}
// session token is valid so we can continue
const newAccessToken = await fetch("path/to/custom/backend") // or a server-side function call
const response = NextResponse.next()
const newSessionToken = await encode({
secret: env.NEXTAUTH_SECRET,
token: {
...otherTokenData,
accessToken: newAccessToken,
},
maxAge: 30 * 24 * 60 * 60, // 30 days, or get the previous token's exp
})
// update session token with new access token
response.cookies.set(sessionCookie, newSessionToken)
return response;
}
return NextResponse.next()
} |
@rinvii Thank you so much for the code! I currently dont have much time for testing but I will do it for sure this weekend, the only missing piece is the |
|
@rinvii the one issue with your solution is that we getting old token in getServerSession after refresh. We need reload page to get new token. |
@rinvii I have reworked and tested your idea and it works!!! I just moved the signOut functionality to a function to make it more clear (as next-auth doesnt provdce a way to sign out server side I think). The only problem, as @arminhupka mentioned, is that the previous session gets stale, so the page needs to get reloaded. I dont know if there is a way to force next-auth to get the new session or if the middleware can force a page reload. import { NextMiddleware, NextRequest, NextResponse } from "next/server";
import { encode, getToken } from 'next-auth/jwt'
export const config = {
matcher: "/protected",
};
const sessionCookie = process.env.NEXTAUTH_URL?.startsWith("https://")
? "__Secure-next-auth.session-token"
: "next-auth.session-token";
function signOut(request: NextRequest) {
const response = NextResponse.redirect(new URL("/api/auth/signin", request.url));
request.cookies.getAll().forEach((cookie) => {
if (cookie.name.includes("next-auth"))
response.cookies.delete(cookie.name);
});
return response;
}
function shouldUpdateToken(token: string) {
// Check the token expiration date or whatever logic you need
return true
}
export const middleware: NextMiddleware = async (request: NextRequest) => {
console.log("Executed middleware")
const session = await getToken({ req: request })
if (!session) return signOut(request)
const response = NextResponse.next()
if (shouldUpdateToken(session.accessToken)) {
// Here yoy retrieve the new access token from your custom backend
const newAccessToken = "Session updated server side!!"
const newSessionToken = await encode({
secret: process.env.NEXTAUTH_SECRET,
token: {
...session,
accessToken: newAccessToken,
},
maxAge: 30 * 24 * 60 * 60, // 30 days, or get the previous token's exp
})
// Update session token with new access token
response.cookies.set(sessionCookie, newSessionToken)
}
return response
} |
@arminhupka @angelhodar Everything is aggressively cached in Next.js https://nextjs.org/docs/app/building-your-application/caching. So I’m going to need more context about the session staleness. Otherwise it just sounds like a caching problem where soft navigations don’t trigger middleware in the way that you want (I think). |
I have tested with the example next-auth template which is based on the pages router. I think the new caching mechanism that seems to be more aggresive and sometimes gives problems is only in the new app router, am I right? The main session staleness problem I think that comes mainly from the SessionProvider, because I suppose it stores the session in memory to just return it in the One possible solution would be to return an extra cookie that tells the client if it has to call the new Edit: I have just read in the docs that the
|
I avoid using the auth HOC and client side functions like useSession. I don’t understand what is meant when the previous session gets stale. An example play by play would be appreciated. And, I store the session in cookies and avoid storing it in client memory. |
@rinvii Thank you so much, your middleware idea actually worked ! I just had to do a small adjustment, because if the encoded token is longer than 3933 characters, Next Auth will split it into multiple tokens with the names cookie-name.[0], cookie-name.[1], cookie-name.[2], etc. so I just had to do a small adjustment to your code import { NextMiddleware, NextRequest, NextResponse } from 'next/server';
import { encode, getToken, JWT } from 'next-auth/jwt';
async function refreshAccessToken(token: JWT): Promise<JWT> {
// implement how you're gonna fetch a new token with the old one
}
export const config = {
matcher: [..your protected routes],
};
const sessionCookie = process.env.NEXTAUTH_URL?.startsWith('https://')
? '__Secure-next-auth.session-token'
: 'next-auth.session-token';
function signOut(request: NextRequest) {
const response = NextResponse.redirect(new URL('/api/auth/signin', request.url));
request.cookies.getAll().forEach((cookie) => {
if (cookie.name.includes('next-auth.session-token')) response.cookies.delete(cookie.name);
});
return response;
}
function shouldUpdateToken(token: JWT): boolean {
// check if you're token is expired
}
export const middleware: NextMiddleware = async (request: NextRequest) => {
const token = await getToken({ req: request });
if (!token) return signOut(request);
const response = NextResponse.next();
if (shouldUpdateToken(token)) {
const newToken = await refreshAccessToken(token);
const newSessionToken = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
token: {
...token,
...newToken,
},
maxAge: 30 * 24 * 60 * 60,
});
const size = 3933; // maximum size of each chunk
const regex = new RegExp('.{1,' + size + '}', 'g');
// split the string into an array of strings
const tokenChunks = newSessionToken.match(regex);
if (tokenChunks) {
tokenChunks.forEach((tokenChunk, index) => {
response.cookies.set(`${sessionCookie}.${index}`, tokenChunk);
});
}
}
return response;
}; |
Hi I have reference this thread in order to refresh the token in middleware when the access token expires and the solution and sample code given by @rinvii was great and I thank you for that Godbless you. As for the issue regarding the old token still being returned by the getServerSession after refresh its because in the middleware it also needs to set the new session cookies at the request object not just the response since the getServerSession would read at the request cookies not the response cookies so basically you need to the set the new session on both the request and response cookies.
it would look like this:
|
Hi guys. Thx for the code. In my case It's working perfectly on the development environment. If I deploy the solutions to the prod it looks like 'response.cookies.set' is not forcing the web browser to set a new cookie. When the token expires the first request is handled properly, the new access token is generated based on the refresh token, the cookie is set properly and the response is sent to the client without logging out the user. But when I refresh the page later it looks like the request from the web browser is still using the old token instead of the new one and in the middelware the code is trying to refresh the token again and fails because the old refresh token was used already. It's only happening on the production environment. Do you have any ideas what might be wrong? UPDATE: I found this open issue Race condition with cookie that might be a cause of the problem here. |
i made some adjustment, when the refresh token is invalid, and I couldn't get a new access token, i just delete the cookies, so the user gets unauthenticated. import { NextMiddleware, NextRequest, NextResponse } from "next/server";
import { JWT, encode, getToken } from "next-auth/jwt";
import { BACKEND_URL } from "./lib/constants";
interface BackendTokens {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
export const config = {
matcher: "/:path*",
};
const sessionCookie = process.env.NEXTAUTH_URL?.startsWith("https://")
? "__Secure-next-auth.session-token"
: "next-auth.session-token";
function signOut(request: NextRequest) {
let response = NextResponse.next();
request.cookies.getAll().forEach((cookie) => {
if (cookie.name.includes("next-auth")) response.cookies.delete(cookie.name);
});
return response;
}
function shouldUpdateToken(tokens: BackendTokens) {
if (new Date().getTime() < tokens.expiresIn) {
return false;
}
return true;
}
export const middleware: NextMiddleware = async (request: NextRequest) => {
const session = await getToken({ req: request });
if (!session) return signOut(request);
let response = NextResponse.next();
if (shouldUpdateToken(session.backendTokens)) {
// Here yoy retrieve the new access token from your custom backend
try {
const newTokens = await refreshToken(session);
const newSessionToken = await encode({
secret: process.env.NEXTAUTH_SECRET!,
token: {
...session,
backendTokens: newTokens,
},
maxAge: 604800 /* TODO: 7 days -> get from the env */,
});
response = updateCookie(newSessionToken, request, response)
} catch (error) {
response = updateCookie(null, request, response)
}
}
return response;
};
async function refreshToken(token: JWT): Promise<BackendTokens> {
const res = await fetch(BACKEND_URL + "/auth/refresh", {
method: "POST",
headers: {
authorization: `Bearer ${token.backendTokens.refreshToken}`,
},
});
const response = await res.json();
if (response.statusCode == 403) {
throw new Error("RefreshTokenError");
}
console.log("refreshed", response);
return response;
}
function updateCookie(
sessionToken: string | null,
request: NextRequest,
response: NextResponse
) {
if (sessionToken) {
// set request cookies for the incoming getServerSession to read new session
request.cookies.set(sessionCookie, sessionToken);
// updated request cookies can only be passed to server if its passdown here after setting its updates
response = NextResponse.next({
request: {
headers: request.headers,
},
});
// set response cookies to send back to browser
response.cookies.set(sessionCookie, sessionToken, {
httpOnly: true,
maxAge: 604800 /* TODO: 7 days -> get from the env */,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
} else {
request.cookies.delete(sessionCookie);
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.delete(sessionCookie)
}
return response;
} |
I’ve tried to debug my problem with the overridden token in the production environment and came to the point that SessionProvider is calling getSession just after the page is reloaded and a new token is generated but it is using old token instead of a newly generated and probably set the old one in the cookie (override newly generated). TBH I am not sure why it is happening in the production environment. |
I don't think the |
Please I was able to update user session by calling update in onClick handler but causing problems when called in a useEffect. Also, the documentation says you can call update() without the page reloading. That doesn't seem to the case. |
`import { getServerSession } from "next-auth"; export default Server;` |
Maybe it has to do with one of the following:
For me, it's working fine in production. I've combined a few of the answers above and deployed to Azure Portal. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Description 📓
Hello everyone. With the Next 13 with RSC by default, we tend to think more about the server-side approach.
For now, I'm not sure quite sure how to update the session on the server side.
Currently, I'm using CredentialProvider which gives me
accessToken
&refreshToken
which I stored in my session.The refreshToken is short-lived, only 30 mins, meaning if the user is idle for 30 mins, it won't work anymore, it will push user back to the login page and delete the session. (Just detailing)
Right now let's say I'm trying to return all the user's project/info on an RSC component. Meaning I'd have to call an API on the server side correct? Meaning everything only happens on the server side.
I'm using axios interceptor for this part, but now I'm only able to read the session (getServerSession), I can't find a way to update the session with the new accessToken/refreshToken. Since we only have
useSession
hook that provides theupdate
method, I'm not sure how we can have that functionality on the server side as well.The functionality I aim for:
If anyone can assist on this, any suggestion is welcome, I'm not 100% sure if my use case is common or correct, but indeed kindly advise since I'm just starting Next 13 and indeed it quite confusing. I've gone through quite a lot of searches but couldn't find this part.
How to reproduce ☕️
N/A
Contributing 🙌🏽
No, I am afraid I cannot help regarding this
The text was updated successfully, but these errors were encountered: