Update Session on Server Side #9715
Replies: 34 comments 56 replies
-
I think there isnt a way to update the session server side yet. I think #6642 is related |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
@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? |
Beta Was this translation helpful? Give feedback.
-
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? |
Beta Was this translation helpful? Give feedback.
-
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()
} |
Beta Was this translation helpful? Give feedback.
-
@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 |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
@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. |
Beta Was this translation helpful? Give feedback.
-
@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
} |
Beta Was this translation helpful? Give feedback.
-
@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). |
Beta Was this translation helpful? Give feedback.
-
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
|
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
@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;
}; |
Beta Was this translation helpful? Give feedback.
-
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:
|
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
`import { getServerSession } from "next-auth"; export default Server;` |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
@dmarkowski actually, re-reading your comment, you should probably stay away from client side functions like getSession() and useSession(). In fact, when using the app router, you don't need a SessionProvider at all. You just export something like
From wherever you have authOptions available. Then you can call
In any server component and pass the session as props to client components. I can guarantee you that this works; I've had it implemented for a few weeks now, and I haven't had any problems. I would recommend to leave out the added complexity of working with large and chunked tokens whenever possible. Instead of saving the ID token in my session cookie, I just have the basics like the access- and refresh token. Just pick an OAuth provider that supports OIDC (most do) and call the /o/userinfo endpoint with the access token. @BowaDAO This might also be useful for you. If you do need to call session hooks from client components though, you should use getSession() to update the session object across all tabs/windows just as @angelhodar pointed out. @hobadams Have you tried implementing the encode logic yourself? It should be really easy, since Auth.js v5 uses general JWT methods available in npm packages to encode and decode its tokens. See: https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/jwt.ts for more info. After this, you can just set the cookies in the middleware |
Beta Was this translation helpful? Give feedback.
-
For anyone who is using Vercel for hosting, it is likely the const sessionCookie = process.env.NEXTAUTH_URL?.startsWith("https://")
? "__Secure-next-auth.session-token"
: "next-auth.session-token"; That means, the sessionCookie will never start with const sessionCookie =
process.env.VERCEL_ENV === 'development'
? 'next-auth.session-token'
: '__Secure-next-auth.session-token' Also, I wrote a blog post that might be helpful. It's about using a dual-auth GitHub provider setup and flow, which includes the middleware here. Thanks everyone for the discussion. |
Beta Was this translation helpful? Give feedback.
-
@maxbec Have you tried playing with options Next.js's fetch implementation provides? If not, you should check out: https://nextjs.org/docs/app/api-reference/functions/fetch. In short, you should be able to do something like this (add to the fetch method in middleware.ts: method: "POST",
next: {
revalidate: 1
} Furthermore, an update on this issue: I've noticed that using server-side functions redirect() and permanentRedirect() right after refreshing the tokens breaks the logic, making it such that subsequent refreshes are attempted with the old, now invalidated refresh token. If you want to redirect from within a server component, you should handle this through a client side function with router.push(). For example: import "server-only";
import React from "react";
import { type Session } from "next-auth";
import { getAuthSession } from "@/lib/auth/AuthOptions";
import AuthRedirect from "@/components/general/AuthRedirect";
export default async function App(): Promise<JSX.Element> {
const session: Session | null = await getAuthSession();
return <AuthRedirect session={session} />;
} And then: "use client";
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import { type Session } from "next-auth";
import { signIn } from "next-auth/react";
export default function AuthRedirect({
session,
url = "/dashboard"
}: Readonly<{ session: Session | null; url?: string }>): JSX.Element {
const router = useRouter();
useEffect(() => {
if (!session?.user) {
signIn("provider name")
.then(() => router.push(url))
.catch((err) => console.error(err));
} else {
try {
router.push(url);
} catch (err) {
console.error(err);
}
}
}, [session, url, router]);
return <div className="flex h-screen items-center justify-center"></div>;
} |
Beta Was this translation helpful? Give feedback.
-
I think I'm missing something. We use the session to store information related to the session, user, server side data, etc. Being able to update the session server side data is a 101 requirement for any auth/session library. Exposing a client-only ability to update a session in an auth library would be unusual, as I think it would be insecure by default. Clients generally shouldn't be able directly update session data. This is why I think I'm missing something, updating the session fundamentally doesn't make sense, this option should not be exposed to clients. The previous comments about hacks to overwrite the cookie value for the session can't be the right way to do this, because it wouldn't make sense for a library that affords sessions to have an insecure first class way to do this on the client, but not on the server. But getServerSession doesn't document how to update the server side session. I feel like I must have some ignorant view here, someone please correct me on my misconception. next-auth is a very popular library, and server session updating is a requirement of libraries that manage sessions, and my understanding is next-auth doesn't have a way to do this, so one of my assumptions in this list is likely wrong. |
Beta Was this translation helpful? Give feedback.
-
I've implemented the middleware suggestion with Okta as a provider as discussed in the some of the comments above (thanks everyone for the help). I now have 1 outstanding issue. I use the access token as a header for some of my API requests. These use a Next API route as a proxy so I can access my token. The issue is, as far as I'm aware the refresh token logic that happens in the middleware happens during client side routing and not API requests (triggered by the client). Has anyone implemented a solution for this? My guess is i need to check the expiry of the access token in the API proxy too and intercept all requests at that point....but then I'm back to the original issue of how to update the clients cookie for there. Any help would be appreciated. Some context In case I didn't explain it well. If you navigate around the app the middleware does it's thing and refreshes the token and browser cookie as and when it expires. If I sit on a single page and don't navigate away (using Next router) until the access token expires (5 mins in my case). I then trigger an API request from the application using a button. This request fails because the access token has expired. |
Beta Was this translation helpful? Give feedback.
-
this way I can update with the update method given. i didn't know that... |
Beta Was this translation helpful? Give feedback.
-
For anyone using the latest beta version (nextauth -> authjs) you need to update the sessionCookie names to:
|
Beta Was this translation helpful? Give feedback.
-
For anyone looking for a way to update the session object from an RSC in v4 the same way you would do on the client-side using import { cookies } from 'next/headers'
export default async function MyPage() {
const cookieStore = cookies()
// or options.cookies.csrfToken.name if you overrides it
// see https://next-auth.js.org/configuration/options#cookies
const csrfToken = cookieStore.get('next-auth.csrf-token')?.value
if (csrfToken) {
// update the session (undocumented next-auth feature)
// see https://github.com/nextauthjs/next-auth/blob/ebfdaece/packages/next-auth/src/react/index.tsx#L474-L479
// and https://github.com/nextauthjs/next-auth/blob/ebfdaece/packages/next-auth/src/core/index.ts#L297-L308
// using the client CSRF Token is required
// see https://github.com/nextauthjs/next-auth/blob/ebfdaece/packages/next-auth/src/core/init.ts#L118-L127
// and https://github.com/nextauthjs/next-auth/blob/ebfdaece/packages/next-auth/src/core/lib/csrf-token.ts#L41
fetch(myAppUrlHelper('/api/auth/session'), {
method: "POST",
headers: {
'Content-Type': 'application/json',
cookie: `next-auth.csrf-token=${csrfToken}`
},
body: JSON.stringify({
csrfToken: csrfToken.split('|')[0],
data: {
foo: 'bar'
}
})
})
}
// ...
} And don't forget to handle it in callbacks: {
async jwt({ user, token, account, trigger, session }): Promise<JWT> {
// ...
if (trigger === 'update') {
return {
...filteredToken,
foo: session.foo
}
}
}
} |
Beta Was this translation helpful? Give feedback.
-
@emreakdas can you demonstrate how you use unstable_update. I am using v5 and want to update the session from the server.
|
Beta Was this translation helpful? Give feedback.
-
How about this ? |
Beta Was this translation helpful? Give feedback.
-
To add yet another comment to this utterly neglected thread: The original question is asking how to update the session on the server side. The code provided by @rinvii in this comment is a large part of the solution: #9715 (comment). But as @arminhupka points out, there are issues with this solution: #9715 (comment). I think the core issue here is multiple, competing A single HTTP response with multiple One can verify this case by observing the HTTP response in chrome dev tools for a call to /api/auth/session where the middleware executed before: it should be a 200, with two Set-Cookie headers. How to handle this? One potential solution would be to 1) exempt running the middleware on Next Auth routes ( Some code snippets for reference: // middleware.ts
export default async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname;
const token = await getToken({ req });
if (!token) {
return NextResponse.next();
}
// Do not perform any token refresh logic for Next auth routes:
// they have their own logic to refresh tokens.
if (path.startsWith('/api/auth')) {
return NextResponse.next();
}
console.log(`🎸 [Middleware] Begin execution for path: ${path}. Request ID: ${requestId}`);
if (shouldUpdateToken(token.authTokenExpires as number)) {
try {
const refreshedTokens = await refreshTokens(
token.refreshToken as string,
token.userId as string
);
updatedSessionToken.authToken = refreshedTokens.authToken;
updatedSessionToken.refreshToken = refreshedTokens.refreshToken;
updatedSessionToken.authTokenExpires = refreshedTokens.authTokenExpires;
} catch (e) {
console.error(
`❌ [Middleware] Error refreshing tokens: ${e}. Request ID: ${requestId}. for path: ${req.nextUrl.pathname}`
);
}
}
const newSessionToken = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
token: updatedSessionToken,
maxAge: 1800, // 30 minutes in seconds
});
console.log(`🎸 [Middleware] Execution complete. Request ID: ${requestId}\n`);
return responseWithUpdatedCookie(newSessionToken, req);
} This code above should work for non-api routes, and loading just regular Next pages. Below demonstrates an augmented Next Auth API route handler with in place session update, by updating the Next Auth produced HTTP response in place. // api/auth/[...nextauth]/route.ts
const handler = NextAuth(authOptions);
// Cookie names.
export const NEXT_AUTH_SESSION_TOKEN_COOKIE = isProduction
? `__Secure-next-auth.session-token`
: `next-auth.session-token`;
const wrappedAuthHandler = async (req: NextRequest, res: NextResponse) => {
const token = await getToken({ req });
const nextAuthResponse = await handler(req, res);
return maybePerformTokenRefresh(token, response);
};
export { wrappedAuthHandler as GET, wrappedAuthHandler as POST };
async function maybePerformTokenRefresh(token: JWT | null, response: Response) {
// Check if there is a Set-Cookie header with the NEXT_AUTH_SESSION_TOKEN_COOKIE name.
// If there is, we need to see if the token is expired and needs an update.
// Return value of getSetCookie() is an array of strings: ['cookie1=value1', 'cookie2=value2', ...]
const setCookieHeaders = response.headers.getSetCookie();
const nextAuthSessionTokenCookie = setCookieHeaders?.find((cookie) =>
cookie.includes(NEXT_AUTH_SESSION_TOKEN_COOKIE)
);
if (!nextAuthSessionTokenCookie) {
// No cookie found, nothing to update.
return response;
}
const cookieValueWithOptions = nextAuthSessionTokenCookie.split('=')[1];
const cookieValue = cookieValueWithOptions.split(';')[0];
const decodedCookieAsToken = await decode({
secret: process.env.NEXTAUTH_SECRET as string,
token: cookieValue,
});
if (
!decodedCookieAsToken ||
!shouldUpdateToken(decodedCookieAsToken.authTokenExpires as number)
) {
return response;
}
const updatedSessionToken = { ...decodedCookieAsToken };
try {
const refreshedTokens = await refreshTokens(
decodedCookieAsToken.refreshToken as string,
decodedCookieAsToken.userId as string
);
updatedSessionToken.authToken = refreshedTokens.authToken;
updatedSessionToken.refreshToken = refreshedTokens.refreshToken;
updatedSessionToken.authTokenExpires = refreshedTokens.authTokenExpires;
} catch (e) {
console.error(`❌❌ [Middleware] Error refreshing tokens: ${e}`);
}
const newSessionToken = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
token: updatedSessionToken,
maxAge: 1800, // 30 minutes in seconds
});
const clonedResponse = response.clone();
clonedResponse.headers.delete('Set-Cookie');
clonedResponse.headers.set(
'Set-Cookie',
createAuthCookieString(newSessionToken)
);
console.log('🍪[Next Auth API Handler] Refreshed token and set cookie');
return clonedResponse;
} With this solution, all cases where a Next Auth session token could be set should be covered, and there should be no competing Set-Cookie header calls. I believe this sufficiently answers the OP's original question of "how to update session server side" in a way that respects an auth token/refresh token based system. |
Beta Was this translation helpful? Give feedback.
-
Hello, if anyone is using version:
The const token = await getToken({
req: request,
secret: process.env.AUTH_SECRET,
salt: "saltHere"
});
const newSession = await encode({
secret: process.env.AUTH_SECRET,
token: {
...token,
access_token: data.accessToken,
refresh_token: data.refreshToken,
},
maxAge: SESSION_TIMEOUT,
salt: "saltHere",
}); I've been receiving the following error once I refresh the session:
As a workaround, I had to override the import { decode as _Decode, encode as _Encode, type JWT } from "next-auth/jwt";
export const authConfig: NextAuthConfig = {
//Configuration...
jwt: {
maxAge: SESSION_TIMEOUT,
decode(params) {
return _Decode({
salt: "saltHere",
secret: process.env.AUTH_SECRET,
token: params.token,
})
},
encode(params) {
return _Encode({
salt: "saltHere",
secret: process.env.AUTH_SECRET,
token: params.token
})
},
}
} Are there any other solutions for this? |
Beta Was this translation helpful? Give feedback.
-
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
Beta Was this translation helpful? Give feedback.
All reactions