logto-io / js

🤓 Logto JS SDKs.
https://docs.logto.io/quick-starts/
MIT License
61 stars 40 forks source link

bug: Next.js getAccessToken() breaking change to getLogtoContext() #778

Closed Damcios-s closed 1 month ago

Damcios-s commented 1 month ago

Describe the bug

The new getAccessToken() server action to get the access token:

let accessToken = await getAccessToken(
        logtoConfig,
        "http://resource/api"
      );

Results in the following error when called on the server:

tA [Error]: Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options

Expected behavior

The old, deprecated way of getting the access token works in the same context:

let context = await getLogtoContext(logtoConfig, {
        getAccessToken: true,
        resource: "http://resource/api",
      });

I would expect the new function to also work in the same context.

How to reproduce?

Create a new Next.js application and set up LogTo. Make a new page.tsx file:

import { getAccessToken, getLogtoContext } from "@logto/next/server-actions";

export default async function Page() {

  // This works
  let context = await getLogtoContext(logtoConfig, {
        getAccessToken: true,
        resource: "http://resource/api",
      });

  // This gives the error
  let accessToken = await getAccessToken(
        logtoConfig,
        "http://resource/api"
      );

  return <></>;
}

Context

"@logto/next": "^3.4.0" "next": "14.2.5",

Intended behavior ?

If this is the intended behavior now, what would be the correct way to get the access token on the server when loading a page - so NOT in a route handler or a server-action? This is quite an important use case for me as I would rather not have to call my backend twice for loading a page that needs authorized data if the user is already signed in.

wangsijie commented 1 month ago

Hi, you can still use the old getLogtoContext method to get access token for now, but it is deprecated and will be removed in the future.

The problem is that HTTP does not allow setting cookies after streaming starts, so we can not cache the access token in the cookie-based session, and resulting the caching failure of access tokens. See the next.js documentation to learn more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options.

And for the usage of new "getAccessToken" method, please refer to this: https://docs.logto.io/quick-starts/next-app-router/#fetch-access-token-for-the-api-resource

Damcios-s commented 1 month ago

Hi, thanks for the reply. I've read both these pieces of documentation, and I think I understand what is happening, but I feel like I'm missing something or an important use case is not covered here.

Correct me if I'm wrong on any of these points below:

The logto docs show how to use the "getAccessToken" method in client components such that it is called as a server action. This means when a page is rendered on the server the access token is not fetched, and only once the page is loaded in the browser will the "getAccessToken" server-action be called, be it with useEffect or a user action (onClick etc).

This means that if I need to load a page that shows data that needs to be fetched from a protected api endpoint, I first need to load the page without the data and then call a server actions to fetch the data and render it to the page later. Even though I already know the user is authenticated while rendering the page I cannot get the access token.

What I'm looking for is getting the access token on the initial page load without having to do a second api call / server action from the browser. I've tried to call "getAccessToken" in middleware but that does not work either:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

import { getAccessToken, getLogtoContext } from "@logto/next/server-actions";
import { logtoConfig } from "@/app/logto";

export async function middleware(request: NextRequest) {
  const { isAuthenticated, claims } = await getLogtoContext(logtoConfig);

  if (!isAuthenticated) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  const accessToken = await getAccessToken(
    logtoConfig,
    "http://registrar:8200/api/admin/v1"
  );

  return NextResponse.next();
}

Maybe I'm missing a good reason why this use case is not supported, or maybe I'm just missing how to do this correctly.

wangsijie commented 1 month ago

You are right, currently it is not able to get access token on the server side (RSC), the root cause is the "stateless" cookie-based session, which makes it hard to update the session value. Maybe we'll need to introduce an option to use other session providers.

wangsijie commented 1 month ago

One temperory solution is to use getLogtoContext in RSC, until we figure out a better method.

wangsijie commented 1 month ago

Hi, we introduced a new function getAccessTokenRSC, the documentation is not ready yet, you can checkout this https://github.com/logto-io/docs/pull/761