Skip to content

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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Session on Server Side #8254

Closed
ChrisLegaxy opened this issue Aug 8, 2023 · 22 comments
Closed

Update Session on Server Side #8254

ChrisLegaxy opened this issue Aug 8, 2023 · 22 comments
Labels
enhancement New feature or request triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@ChrisLegaxy
Copy link

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 the update method, I'm not sure how we can have that functionality on the server side as well.

The functionality I aim for:

  1. axios call in server side with the accessToken
  2. If the accessToken expire, it has the interceptor fall back to refreshToken
  3. Then it updates the next auth session with the new accessToken/refreshToken, then retries the request

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

@ChrisLegaxy ChrisLegaxy added enhancement New feature or request triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Aug 8, 2023
@angelhodar
Copy link

angelhodar commented Aug 11, 2023

I think there isnt a way to update the session server side yet. I think #6642 is related

@Praiz001
Copy link

Praiz001 commented Aug 22, 2023

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.

@rinvii
Copy link

rinvii commented Aug 23, 2023

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.

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 the update method, I'm not sure how we can have that functionality on the server side as well.

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.

@angelhodar
Copy link

@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?

@mistercorea
Copy link

mistercorea commented Aug 23, 2023

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?

@rinvii
Copy link

rinvii commented Aug 23, 2023

@angelhodar @mistercorea

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()
}

@angelhodar
Copy link

angelhodar commented Aug 23, 2023

@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 encode function that I dont know where is exported from next-auth. I think this should be added to the next auth documentation, I have seen some issues that could be solved with this. Maybe its planned to be added in v5? Any maintainer can confirm?

@arminhupka
Copy link

@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 encode function that I dont know where is exported from next-auth. I think this should be added to the next auth documentation, I have seen some issues that could be solved with this. Maybe its planned to be added in v5? Any maintainer can confirm?

import {encode} from 'next-auth/jwt'

@arminhupka
Copy link

@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.

@angelhodar
Copy link

angelhodar commented Aug 27, 2023

@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
}

@rinvii
Copy link

rinvii commented Aug 27, 2023

@arminhupka @angelhodar Everything is aggressively cached in Next.js https://nextjs.org/docs/app/building-your-application/caching. So I鈥檓 going to need more context about the session staleness. Otherwise it just sounds like a caching problem where soft navigations don鈥檛 trigger middleware in the way that you want (I think).

@angelhodar
Copy link

angelhodar commented Aug 27, 2023

@arminhupka @angelhodar Everything is aggressively cached in Next.js https://nextjs.org/docs/app/building-your-application/caching. So I鈥檓 going to need more context about the session staleness. Otherwise it just sounds like a caching problem where soft navigations don鈥檛 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 useSession hook instead of making a network request everytime the hook is called (I dont know internally how it works but makes sense)

One possible solution would be to return an extra cookie that tells the client if it has to call the new update method from the useSession hook to force a session reload. That could be implemented in a background component that just executes its useEffect on every route change. I am sure that there is a more elegant solution to force next-auth to refresh the session but I dont know how to do it.

Edit: I have just read in the docs that the update method doesnt sync across tabs, so the getSession function should be called as its specified:

If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can trigger an update of the session object across all tabs/windows by calling聽getSession()聽from a client side function

@rinvii
Copy link

rinvii commented Aug 27, 2023

I avoid using the auth HOC and client side functions like useSession. I don鈥檛 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.

@fivaz
Copy link

fivaz commented Sep 24, 2023

@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;
};

@mabutalid
Copy link

mabutalid commented Oct 24, 2023

@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.

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.

  • request cookies for the getServerSession to read
  • response cookies to send back to the browser

it would look like this:

// set request cookies for the incoming getServerSession to read new session
      tokenChunks.forEach((tokenChunk, index) => {
        req.cookies.set(`${sessionCookie}.${index}`, tokenChunk);
      });

      // updated request cookies can only be passed to server if its passdown here after setting its updates
      const response = NextResponse.next({
        request: {
          headers: req.headers,
        },
      });

      // set response cookies to send back to browser
      tokenChunks.forEach((tokenChunk, index) => {
        response.cookies.set(`${sessionCookie}.${index}`, tokenChunk, {
          httpOnly: true,
          maxAge: 1 * 24 * 60 * 60, // 1 day
          secure: !!isCookieSecure,
          sameSite: "lax",
        });
      });

      console.log("TOKEN REFRESH SUCCESSFUL...");

      return response; 

@dmarkowski
Copy link

dmarkowski commented Nov 7, 2023

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.

@kisstamasj
Copy link

kisstamasj commented Nov 17, 2023

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;
}

@dmarkowski
Copy link

dmarkowski commented Nov 20, 2023

I鈥檝e 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.

image
override_token

@hobadams
Copy link

I don't think the encode method is exported in v5 (import { encode } from 'next-auth/jwt'; is deprecated). Anyone any ideas?

@kokombo
Copy link

kokombo commented Jan 8, 2024

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.

@Yassin-Samir
Copy link

Yassin-Samir commented Jan 16, 2024

`import { getServerSession } from "next-auth";
import { NextAuthOptions } from "./api/auth/[...nextauth]/route";
async function Server() {
const session = await getServerSession(NextAuthOptions);
console.log({ server: session });
}

export default Server;`
place authoptions in getserversession

@NanningR
Copy link

I鈥檝e 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.

image override_token

Maybe it has to do with one of the following:

  • When you set the response cookie, you don't set the "secure" attribute. It should be something like: secure: process.env.NODE_ENV === "production",
  • You need to take into account the fact that tokens will be split up into chunks if they become too large

For me, it's working fine in production. I've combined a few of the answers above and deployed to Azure Portal.

@nextauthjs nextauthjs locked and limited conversation to collaborators Jan 22, 2024
@balazsorban44 balazsorban44 converted this issue into discussion #9715 Jan 22, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
enhancement New feature or request triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests