Skip to content
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

Next.js Amplify v6 Server side Authorization header issue #12971

Open
3 tasks done
asp3 opened this issue Feb 7, 2024 · 12 comments · May be fixed by #12979
Open
3 tasks done

Next.js Amplify v6 Server side Authorization header issue #12971

asp3 opened this issue Feb 7, 2024 · 12 comments · May be fixed by #12979
Assignees
Labels
Auth Related to Auth components/category feature-request Request a new feature GraphQL Related to GraphQL API issues SSR Issues related to Server Side Rendering

Comments

@asp3
Copy link

asp3 commented Feb 7, 2024

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

auth, api

Backend

Amplify CLI

Environment information

  System:
    OS: macOS 14.0
    CPU: (12) arm64 Apple M2 Max
    Memory: 64.38 MB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.6.1 - ~/.nvm/versions/node/v20.6.1/bin/node
    Yarn: 1.22.5 - ~/.yarn/bin/yarn
    npm: 9.8.1 - ~/.nvm/versions/node/v20.6.1/bin/npm
    Watchman: 2023.10.23.00 - /usr/local/bin/watchman
  Browsers:
    Brave Browser: 114.1.52.130
    Chrome: 121.0.6167.139
    Safari: 17.0
  npmPackages:
    @knowt/eslint-config: * => 0.0.0 
    dotenv-cli: latest => 7.3.0 
    husky: ^8.0.0 => 8.0.3 
    lint-staged: ^12.4.0 => 12.5.0 
    prettier: ^2.7.1 => 2.8.8 
    turbo: ^1.10.12 => 1.10.16 
  npmGlobalPackages:
    amplify: 0.0.11
    appcenter-cli: 2.14.0
    corepack: 0.19.0
    eas-cli: 5.4.0
    eslint: 8.56.0
    expo-cli: 6.3.10
    npm-check: 6.0.1
    npm: 9.8.1

Describe the bug

On client side, I am able to make all calls as expected, by passing an id token in the header.

Amplify.configure(awsConfig, {
    ssr: true,
    API: {
        GraphQL: {
            headers: async () => ({
                Authorization: (await fetchAuthSession()).tokens?.idToken?.toString(),
            }),
        },
    },
});
export const serverClient = generateServerClientUsingCookies({
    config: awsConfig,
    cookies,
});

however, on server side, we are not allowed to override the headers from this function, so there is no way to use the id token!

I saw a few threads (#12699) but it seems unnecessarily complex where we have to wrap every call in runWithAmplifyServerContextCore, rather than just use the idToken already readily available in the cookies. Wondering if there was any fix for that!

Expected behavior

Expected server side calls are also made with the proper ID token claims. Currently, it seems to be making it with the access token, with no way to override.

Reproduction steps

  1. setup next.js app with client and server side, using id token claims override
  2. observe that client calls work properly, but server calls do not.

Code Snippet

No response

Log output

No response

aws-exports.js

No response

Manual configuration

No response

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

@asp3 asp3 added the pending-triage Issue is pending triage label Feb 7, 2024
@asp3 asp3 changed the title Amplify v6 Server side Authorization header issue Next.js Amplify v6 Server side Authorization header issue Feb 7, 2024
@nadetastic nadetastic added Auth Related to Auth components/category SSR Issues related to Server Side Rendering labels Feb 7, 2024
@nadetastic nadetastic self-assigned this Feb 7, 2024
@nadetastic nadetastic added the investigating This issue is being investigated label Feb 7, 2024
@asp3
Copy link
Author

asp3 commented Feb 8, 2024

bumping on this issue, @nadetastic would you know of any workaround in the meantime to allow idToken use from server side graphql API calls?

@asp3
Copy link
Author

asp3 commented Feb 8, 2024

so far, my workaround has been:

const idToken = cookies()
    .getAll()
    .filter(
        cookie => cookie.name.includes("CognitoIdentityServiceProvider") && cookie.name.includes("idToken")
    )?.[0]?.value;

export const serverClient = generateServerClientUsingCookies({
    config: awsConfig,
    cookies,
    ...(idToken
        ? { authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, authToken: idToken }
        : {
              authMode: GRAPHQL_AUTH_MODE.AWS_IAM,
          }),
}) as V6ClientSSRCookies;

but this seems pretty messy :)

@asp3
Copy link
Author

asp3 commented Feb 8, 2024

So my previous workaround didn't end up working, but I was able to just use a patch file to just switch the token thats passed from the graphql call to just use idToken.

diff --git a/node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs b/node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs
index 3f1a5f7..b676d26 100644
--- a/node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs
+++ b/node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs
@@ -58,7 +58,7 @@ class InternalGraphQLAPIClass {
             case 'userPool':
                 try {
                     let token;
-                    token = (await amplify.Auth.fetchAuthSession()).tokens?.accessToken.toString();
+                    token = (await amplify.Auth.fetchAuthSession()).tokens?.idToken.toString();
                     if (!token) {
                         throw new Error(GraphQLAuthError.NO_FEDERATED_JWT);
                     }
@@ -67,7 +67,7 @@ class InternalGraphQLAPIClass {
                     };
                 }
                 catch (e) {
-                    throw new Error(GraphQLAuthError.NO_CURRENT_USER);
+                    // pass, call with no authorization header
                 }
                 break;
             case 'lambda':

using patch-package for anyone who runs into the same issue. The second part of this code (removing the throw new Error...) allows it to fallback to IAM if the user is not set, while maintaing the default auth mode of userPool (#12931). @johnf I saw you commented there with this issue, so maybe doing this patch for now will be helpful.

@nadetastic
Copy link
Contributor

nadetastic commented Feb 13, 2024

Hi @asp3,

Thank you for opening this issue, and for your patience. I see you would like to set an Authorization header at the config level for to be used with your GraphQL requests. To achieve this, you should consider injecting the authorization token with each api call through client.graphql() instead since the server context is being injected on each API call. Here is an example:

await runWithAmplifyServerContext({
	nextServerContext: { cookies },
	operation: async (contextSpec) => {
		return client.graphql(
			contextSpec,
			{
				query: listTodos,
			},
			{
				// `Authorization` also works here
	            authToken: ( await fetchAuthSession(contextSpec) ).tokens?.idToken?.toString() as string, 
			}
		);
	},
});

Additionally, I saw your contribution at #12979 and are reviewing it with the team - I will follow up on that PR shortly. In the meantime, let me know if you have any additional questions regarding this issue.

@nadetastic nadetastic added GraphQL Related to GraphQL API issues pending-response Issue is pending response from the issue requestor and removed pending-triage Issue is pending triage investigating This issue is being investigated labels Feb 13, 2024
@asp3
Copy link
Author

asp3 commented Feb 20, 2024

Hi @nadetastic, thanks for the reply! Doing it this way also works, but I am now running into the issue of TooManyRequestsException (I assume from fetchAuthSession). Since this token is already available in cookies, it seems like overkill to fetch the full auth session each time (#12839). Any ideas on what I can do to be unblocked here? We need the id token from each server side call, and we definitely make a few hundred within 10-15 seconds regularly.

@github-actions github-actions bot removed the pending-response Issue is pending response from the issue requestor label Feb 20, 2024
@asp3
Copy link
Author

asp3 commented Feb 21, 2024

@nadetastic this is a p big blocking issue since it leads to 404s on certain pages. any workaround you would know of? Thanks in advance :)

@chrisbonifacio
Copy link
Contributor

Hi @asp3, after reading through #12839 it seems the options you have are:

  1. offload some of your requests to the client side which can take advantage of caching the credentials
  2. create a custom server action wrapper that can batch and run multiple operations in the same server context so credentials are only fetched once
  3. create and use a custom graphql mutation that batches the DynamoDB operations

Have you explored these options?

@nadetastic nadetastic added the pending-response Issue is pending response from the issue requestor label Mar 7, 2024
@willianrod
Copy link

I had the same issue, to solve it I started calling fetchAuthSession every time, but then I started receiving the TooManyRequestsException exception. So I started calling fetchAuthSession only when the token was expired.

import { cookies } from "next/headers";

import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
import { getIdToken, getToken } from "@company/ui/lib";
import { fetchAuthSession } from "aws-amplify/auth/server";

import { awsConfig } from "../aws-exports";

export const { runWithAmplifyServerContext } = createServerRunner({
  config: awsConfig,
});

export const getServerClient = async () => {
  const idToken = cookies()
    .getAll()
    .filter(
        cookie => cookie.name.includes("CognitoIdentityServiceProvider") && cookie.name.includes("idToken")
    )?.[0]?.value;

  const token = await runWithAmplifyServerContext({
    nextServerContext: { cookies },
    operation: async (contextSpec) => {
      try {
        const parsedToken = jwtDecode(idToken);
        const isExpired =
          parsedToken?.exp && parsedToken.exp * 1000 < Date.now();

        if (!isExpired) return idToken;

        const session = await fetchAuthSession(contextSpec);
        return session.tokens.idToken.toString();
      } catch (error) {
        return false;
      }
    },
  });

  return generateServerClientUsingCookies({
    config: awsConfig,
    cookies,
    authMode: "userPool",
    authToken: token,
  });
};

@asp3
Copy link
Author

asp3 commented Mar 12, 2024

@willianrod thanks for the idea, did something very similar!

const getServerAuth = async (): Promise<Pick<ServerClientWithCookies, "auth">> => {
    const cookieStore = cookies();

    let idToken = cookieStore
        .getAll()
        .filter(
            cookie => cookie.name.includes("CognitoIdentityServiceProvider") && cookie.name.includes("idToken")
        )?.[0]?.value;

    if (!idToken) {
        return {
            auth: {
                authMode: GRAPHQL_AUTH_MODE.AWS_IAM,
            },
        };
    }

    const parsedToken = jwtDecode(idToken);
    const isExpired = parsedToken.exp && parsedToken.exp + TOKEN_BUFFER <= now();

    if (isExpired) {
        // refresh the token
        const currentUser = await runWithAmplifyServerContext({
            nextServerContext: { cookies },
            operation: contextSpec =>
                fetchAuthSession(contextSpec, {
                    forceRefresh: true,
                }),
        });

        if (!currentUser.tokens?.idToken) {
            return {
                auth: {
                    authMode: GRAPHQL_AUTH_MODE.AWS_IAM,
                },
            };
        }

        idToken = currentUser.tokens.idToken.toString();
    }

    return {
        auth: {
            authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
            authToken: idToken,
        },
    };
};

However, I do think that this is something that should be fixed from Amplify's side. Having to fetch the token on every call is very inefficient, especially in a world where more and more things are being rendered on the server.

@github-actions github-actions bot removed the pending-response Issue is pending response from the issue requestor label Mar 12, 2024
@chrisbonifacio
Copy link
Contributor

Thank you for sharing your workaround, @willianrod, and helping to unblock @asp3.
Marking this as a feature request to discuss with the team.

@chrisbonifacio chrisbonifacio added the feature-request Request a new feature label Mar 28, 2024
@asp3
Copy link
Author

asp3 commented Apr 2, 2024

hey @chrisbonifacio @willianrod, running into an interesting issue, even after setting up the propsed solution above.

From what I can see, there are still call made to Congito IDP each for every single request, even though all authenitcation information has been passed into the graphql call. any ideas on why this would be happening?

I verified that this was the path of a request where the id token is not expired, so before the graphql call, no calls to fetchAuthSession were made from my end!

Capster 02-04-2024 @ 06 32 44

@willianrod
Copy link

@asp3 @chrisbonifacio

Not sure if that is what is happening for you but when I was migrating from v5 to v6 I couldn't find anywhere in the docs mentioning the headers for graphql calls, so my guess is you may not be explicitly passing the auth headers and graphql client is doing some requests under the hood to get a new token every time.

When using Amplify.configure you can pass a second parameter with some API configs, and I did the same thing there by checking if the token is expired and then try to get a new one if expired. (I found it by digging the source code)

Here's a sample code

Amplify.configure(awsConfig, {
  ssr: true,
  API: {
    GraphQL: {
      headers: async () => {
        return {
          Authorization: getAuthToken(),
        };
      },
    },
  },
});

export default function AmplifyProvider({
  children,
}: React.PropsWithChildren) {
  return <>{children}</>;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auth Related to Auth components/category feature-request Request a new feature GraphQL Related to GraphQL API issues SSR Issues related to Server Side Rendering
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants