aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.4k stars 2.11k forks source link

"No current user" error when using custom tokenProvider with Next.JS server components #12699

Open HieronymusLex opened 7 months ago

HieronymusLex commented 7 months ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

GraphQL API

Amplify Categories

api

Environment information

``` # Put output below this line Binaries: Node: 18.19.0 - ~/.nvm/versions/node/v18.19.0/bin/node npm: 10.2.3 - ~/.nvm/versions/node/v18.19.0/bin/npm npmPackages: aws-amplify: ^6.0.6 => 6.0.6 next: 14.0.4 => 14.0.4 next-auth: ^5.0.0-beta.4 => 5.0.0-beta.4 ```

Describe the bug

I have an AppSync API configured to use OIDC based auth. My project does not use the Auth module or Cognito

Per the Custom Token Providers section in the docs, I have configured a tokenProvider which obtains the tokens from the next-auth package (which in turn is using my 3rd auth provider). I now want to use these to authenticate requests to AppSync.

The tokenProvider does not work with server rendered components which call the graphql API using cookiesClient.graphql where the cookiesClient is obtained from generateServerClientUsingCookies - instead a No current user error is thrown (caused by this code).

From debugging, I can see that when InternalGraphQLAPIClass._headerBasedAuth calls amplify.Auth.fetchAuthSession it calls getTokens and the authOptions on the AuthClass singleton are null at this point. Important this issue only occurs in server rendered components. The token provider works as expected in client rendered components.

I'm unsure but my current thought is that this is occurring because the library options are not being passed through correctly in the adapter-nextjs code - the code here indicates that only the resourcesConfig is being considered, not the library options. In my scenario resourcesConfig.Auth is undefined, but it even so on L41 it is always setting the token provider to the cognito user pool token provider anyways

Expected behavior

The custom token provider should be used when making graphql calls from both server and client components

Reproduction steps

  1. Configure a NextJS app with next-auth and a 3rd party provider for OIDC tokens. The NextAuth sample repo is a good starting point
  2. Configure a custom token provider for next auth
  3. Init an amplify project, add an API and select OIDC as the auth method (Todo example is fine)
  4. Make a graphql call from a server rendered component as per the docs

Code Snippet

// Put your logs below this line
// pass this to Amplify.configure - see manual configuration
export const tokenProvider = {
  async getTokens({ forceRefresh } = {}) {
    const session = await auth() // implementation: https://github.com/nextauthjs/next-auth-example/blob/main/auth.ts
    // error handling removed for brevity
    return {
        accessToken: decodeJWT(session?.user.access_token),
        idToken: decodeJWT(session?.user.id_token),
      }
  },
}

// in the server component
const cookiesClient = generateServerClientUsingCookies({
    config,
    cookies,
});
const {data, errors} = await cookiesClient.graphql({
    query: queries.listTodos,
});

Log output

aws-exports.js

No response

Manual configuration

Amplify.configure(config, {
  ssr: true,
  Auth: {
    tokenProvider
  }
})

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

HuiSF commented 7 months ago

Hi @HieronymusLex thanks for reaching out.

The singleton method Amplify.configure() does not take any effect on the server side usage. The custom token provider should be configured with the createServerRunner function exported from the @aws-amplify/adapter-nextjs, we haven't not yet, however, roll out the support. We will work on this.

In the meantime, you may consider to directly use the lower level SSR adapter function runWithAmplifyServerContext exported from the aws-amplify/adapter-core, where you can full freedom to passing custom token provider. Usage example see here.

I will mark this issue as a feature request for now.

HieronymusLex commented 7 months ago

@HuiSF Thanks for the update and workaround! I think it would be worth calling some of these features out in the docs and adding some an example for Next.JS regarding the custom token provider section.

Regarding using the runWithAmplifyServerContext from the adapter-core, do you have an example of how to make a graphql call using this? My code is as follows:

const results = await runWithAmplifyServerContextCore(
    config,
    {
      Auth: {
        tokenProvider
      }
    },
    async (...args) => {
      const cookiesClient = generateServerClientUsingCookies({
        config,
        cookies,
        authMode: "oidc",
      });
      const {data, errors} = await cookiesClient.graphql({
        query: queries.listTodos,
      });
      return data
    });

I'm assuming my error is getting the client from generateServerClientUsingCookiesbut I'm not sure how else to get the client?

HieronymusLex commented 7 months ago

just following up here with the following code which is working:

const reqResBasedClient = generateServerClientUsingReqRes({
  config
});

const results = await runWithAmplifyServerContextCore(
  config,
  {
    Auth: {
      tokenProvider
    }
  },
  async (contextSpec) => {
    const {data, errors} = await reqResBasedClient.graphql(contextSpec, {
      query: queries.listTodos,
    });
    return data
  }
);
harryy2510 commented 6 months ago

I've been struggling with the exact same issue and exact same codebase 😄

export const {
  handlers: { GET, POST },
  auth,
  signOut,
  signIn,
} = NextAuth({
  secret: config.auth.authSecret,
  providers: [provider],
  pages: {
    signIn: '/api/auth/sign-in',
  },
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        return produce(token, (draft) => {
          draft.accessToken = account.access_token;
          draft.idToken = account.id_token;
          draft.refreshToken = account.refresh_token;
        });
      }
      return token;
    },
    async session({ session, token }) {
      return produce(session, (draft) => {
        draft.accessToken = token.accessToken as string;
        draft.idToken = token.idToken as string;
        draft.refreshToken = token.refreshToken as string;
        draft.user.id = token.sub!;
      });
    },
  },
});
const nextAuthTokenProvider: TokenProvider = {
  async getTokens() {
    const session = await auth();
    if (!session) {
      return null;
    }
    const accessTokenString = session.accessToken;
    const idTokenString = session.idToken;
    return {
      accessToken: decodeJWT(accessTokenString),
      idToken: decodeJWT(idTokenString),
    };
  },
};

export async function configureAmplify() {
  Amplify.configure(amplifyConfig, {
    ssr: true,
    Auth: {
      tokenProvider: nextAuthTokenProvider,
    },
  });
}

@HieronymusLex Your solution looks great for the SSR. Do you also happen to have something for the client side working? I mean I'm also trying to generate a client for client side with the same session details from next-auth. Would appreciate your help here!

harryy2510 commented 6 months ago

ooo. I created a server action, and and called from client 😍 worked great for me. Thanks for the direction here. Your code snippet helped me get into the right direction, thanks again.