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.42k stars 2.12k forks source link

Next.js and AWS Amplify - `custom_header` defined in Amplify.configure() doesn't work with SSR Calls (REST, not GraphQL) #10290

Closed phstc closed 10 months ago

phstc commented 2 years ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

Authentication, REST API

Amplify Categories

auth, api

Environment information

``` System: OS: macOS 12.5 CPU: (10) arm64 Apple M1 Max Memory: 7.33 GB / 64.00 GB Shell: 5.8.1 - /bin/zsh Binaries: Node: 16.16.0 - ~/.volta/tools/image/node/16.16.0/bin/node Yarn: 1.22.19 - ~/.volta/bin/yarn npm: 8.11.0 - ~/.volta/tools/image/node/16.16.0/bin/npm Browsers: Chrome: 104.0.5112.101 Safari: 15.6 npmPackages: @ampproject/toolbox-optimizer: undefined () @aws-amplify/ui-react: ^3.4.1 => 3.4.1 @aws-amplify/ui-react-internal: undefined () @aws-amplify/ui-react-legacy: undefined () @babel/core: undefined () @babel/runtime: 7.15.4 @edge-runtime/primitives: 1.1.0-beta.26 @hapi/accept: undefined () @headlessui/react: ^1.6.6 => 1.6.6 @heroicons/react: ^1.0.6 => 1.0.6 @napi-rs/triples: undefined () @next/react-dev-overlay: undefined () @segment/ajv-human-errors: undefined () @tailwindcss/forms: ^0.5.2 => 0.5.2 @vercel/nft: undefined () acorn: undefined () amphtml-validator: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () autoprefixer: ^10.4.8 => 10.4.8 aws-amplify: ^4.3.33 => 4.3.33 babel-packages: undefined () babel-plugin-inline-react-svg: ^2.0.1 => 2.0.1 browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () chalk: undefined () ci-info: undefined () cli-select: undefined () comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () content-disposition: undefined () content-type: undefined () cookie: undefined () cross-spawn: undefined () crypto-browserify: undefined () cssnano-simple: undefined () debug: undefined () devalue: undefined () domain-browser: undefined () edge-runtime: undefined () eslint: 8.22.0 => 8.22.0 eslint-config-next: 12.2.5 => 12.2.5 events: undefined () find-cache-dir: undefined () find-up: undefined () fresh: undefined () get-orientation: undefined () glob: undefined () gzip-size: undefined () http-proxy: undefined () https-browserify: undefined () icss-utils: undefined () ignore-loader: undefined () image-size: undefined () is-animated: undefined () is-docker: undefined () is-wsl: undefined () jest-worker: undefined () json5: undefined () jsonwebtoken: undefined () loader-utils: undefined () lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () neo-async: undefined () next: 12.2.5 => 12.2.5 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () postcss: ^8.4.16 => 8.4.16 (8.4.14) postcss-flexbugs-fixes: undefined () postcss-modules-extract-imports: undefined () postcss-modules-local-by-default: undefined () postcss-modules-scope: undefined () postcss-modules-values: undefined () postcss-preset-env: undefined () postcss-safe-parser: undefined () postcss-scss: undefined () postcss-value-parser: undefined () process: undefined () punycode: undefined () querystring-es3: undefined () raw-body: undefined () react: 18.2.0 => 18.2.0 (18.0.0) react-dom: 18.2.0 => 18.2.0 react-is: 17.0.2 react-refresh: 0.12.0 react-server-dom-webpack: undefined () regenerator-runtime: 0.13.4 sass-loader: undefined () schema-utils: undefined () semver: undefined () send: undefined () setimmediate: undefined () source-map: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () tailwindcss: ^3.1.8 => 3.1.8 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () ua-parser-js: undefined () unistore: undefined () util: undefined () vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () npmGlobalPackages: corepack: 0.10.0 npm: 8.11.0 ```

Describe the bug

I'm trying to use getServerSideProps + SSR.API.get (REST, not GraphQL) and I'm getting "No current user". If I move the code to useEffect, it works fine.

export async function getServerSideProps({req}) {
  const { Auth, API } = withSSRContext({req});

  try {
    const apiName = "API";
    const path = `/user/self`;

    // throws No current user
    user = await API.get(apiName, path);

    return { props: { user } }    
  } catch (e) {
    console.error(e);
    throw e
  }
}

The API Authorization is configured (it works with useEffect), and I also have SSR enabled ssr: true.

  // configuration
  API: {
    endpoints: [
      {
       /* ... */
        custom_header: async () => {
          return {
            Authorization: `Bearer ${(await Auth.currentSession())
              .getIdToken()
              .getJwtToken()}`,
          };
        },
      },
    ],
  },
  ssr: true,

I tried to re-set the Authorization, although SSR.Auth.currentSession() works fine. I could get a token, but I'm still getting "No current user".

export const getServerSideProps = async ({ req }) => {
  const SSR = withSSRContext({ req });

    const options = {
      headers: {
        Authorization: `Bearer ${(await SSR.Auth.currentSession())
          .getIdToken()
          .getJwtToken()}`,
      },
    };

    const user = await SSR.API.get("API", "users/self", options); // throws No current user

Could it be my version of next: 12.2.5?

I found a similar issue on StackOverflow but no answers in there.

Expected behavior

Be able to make authorized requests from getServerSideProps.

Reproduction steps

  1. Use REST API instead of GraphQL
  2. Try to make an authorized call from getServerSideProps

Code Snippet

// Put your code below this line.

Log output

``` // Put your logs below this line ```

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

phstc commented 2 years ago

Could it be my version of next: 12.2.5?

Quick update: I tried with nextjs 11.1.4 and got the same error.

abdallahshaban557 commented 2 years ago

Ideally this should work with REST API - we had tested this just recently. Are you changing your cookiestorage strategy at all from the Auth category?

phstc commented 2 years ago

@abdallahshaban557 no changes in the cookiestorage.

This is my entire Amplify config:


const amplifyConfig = {
  ssr: true,

  Auth: {
    identityPoolId: process.env.NEXT_PUBLIC_IDENTITY_POOL_ID,
    region: process.env.NEXT_PUBLIC_REGION,
    userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID,
    userPoolWebClientId: process.env.NEXT_PUBLIC_USER_POOL_WEB_CLIENT_ID,
  },

  API: {
    endpoints: [
      {
        name: "API",
        endpoint: process.env.NEXT_PUBLIC_ENDPOINT,
        custom_header: async () => {
          return {
            Authorization: `Bearer ${(await Auth.currentSession())
              .getIdToken()
              .getJwtToken()}`,
          };
        },
      },
    ],
  },

  Storage: {
    AWSS3: {
      bucket: process.env.NEXT_PUBLIC_BUCKET,
      region: process.env.NEXT_PUBLIC_REGION,
    },
  },
};
phstc commented 2 years ago

I'm currently using fetch, and it is working fine.

    const options = {
      headers: {
        Authorization: `Bearer ${(await SSR.Auth.currentSession())
          .getIdToken()
          .getJwtToken()}`,
      },
    };

    // const user = await SSR.API.get("API", "users/self"); // throws No current user
    // const user = await SSR.API.get("API", "users/self", options); // throws No current user
   const response = await fetch("URL...users/self", { ...options, method: "GET" }); // works

It seems to be something specific with API.

abdallahshaban557 commented 2 years ago

Interesting - thank you for the update! We will check this and update you!

tannerabread commented 1 year ago

Hi :wave: @phstc!

I have reproduced this on the server with a manual configuration of some old cognito and API resources, but the problem does not happen if new resources are created from the Amplify CLI.

It also does not work for me by calling it in useEffect but I was able to obtain the resources from getServerSideProps with fetch.

I have a few follow up questions so that we can figure out the root cause:

  1. Which type of Auth do you have set up for this project? API Key or Cognito?
  2. If it is API key, what is the expiration on the API key? Is it expired?
phstc commented 1 year ago

@tannerabread thanks for looking into it.

  1. Cognito.
  2. ☝️

Sample CDK + Lambda.

    const restApiAuthorizer = new apiGateway.CfnAuthorizer(
      this,
      "APIAuthorizer",
      {
        restApiId: restApi.restApiId,
        name: "APIAuthorizer",
        type: AuthorizationType.COGNITO,
        identitySource: "method.request.header.Authorization",
        providerArns: [props.userPool.userPoolArn],
      }
    );

    guideWithIdResource.addMethod("GET", new LambdaIntegration(readFn), {
      authorizationType: AuthorizationType.COGNITO,
      authorizer: {
        authorizerId: restApiAuthorizer.ref,
      },
    });
tannerabread commented 1 year ago

@phstc I was finally able to get passed the "No current user" error.

It seems like the custom header in your amplify configure might be causing the issue. I did quite a bit of digging and logging and noticed that the error was actually being thrown from AuthClass and the more specific error was "Failed to get user from user pool"

I was able to get rid of the error by removing the entire custom_header section from Amplify.configure.

Personally, I am now getting 401/403 errors but I believe that is a different issue from how I set up this API.

Please let me know if this solves the issue for you as well

EDIT: Another note, it works successfully when I manually configure the project with the resources that I made through the CLI, so my 401/403 errors were definitely from some bad resource setup on my end.

My final configuration was as follows:

Amplify.configure({
  Auth: {
    region: "XX-XXXX-X",
    userPoolId: "XX-XXXX-X_abcd1234",
    userPoolWebClientId: "a1b2c3d4e5f6g7h8i9j0k1l2m3",
    identityPoolId: "XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab",
  },
  API: {
    endpoints: [
      {
        name: "apiName",
        endpoint: "https://12a3bcdefg.execute-api.us-east-1.amazonaws.com/dev/",
        region: "XX-XXXX-X"
      },
    ],
  },
  ssr: true,
});

This in turn allowed me to use API.get as expected in getServerSideProps:

export async function getServerSideProps({ req }) {
  const apiName = "apiName";
  const path = "items";

  const SSR = withSSRContext({ req });
  const response = await SSR.API.get(apiName, path);

  return {
    props: {
      response,
    },
  };
}
tannerabread commented 1 year ago

I played around with it some more this morning and was able to get the other resources working as well.

For my cognito user pool and REST API set up outside of this project, I had to set COGNITO_USER_POOLS as the authorization in the API Gateway console for both /items and /{proxy+}: image

This allowed me to use the configuration from my last comment and getServerSideProps like this:

export async function getServerSideProps({ req }) {
  const SSR = withSSRContext({ req });

  const apiName = "apiName";
  const path = "items";
  const myInit = {
    headers: {
      Authorization: `Bearer ${(await SSR.Auth.currentSession())
        .getIdToken()
        .getJwtToken()}`,
    },
  };

  const response = await SSR.API.get(apiName, path, myInit);

  return {
    props: {
      response,
    },
  };
}

The SSR docs here explain that

withSSRContext utility creates an instance of Amplify scoped to a single request (req) using those cookie credentials.

I think the custom_header config authorization was improperly hydrating the scoped Amplify instance with a promise instead of the results of Auth.currentSession()

tannerabread commented 1 year ago

Hi :wave: @phstc Are you still having problems with this issue? Did my findings help you out?

tannerabread commented 1 year ago

Hi 👋 Closing this as resolved. If you are still experiencing this issue and in need of assistance, please feel free to comment and provide us with additional information so we can re-open this issue and be better able to assist you.

Thank you!

phstc commented 1 year ago

hi @tannerabread

I'm using COGNITO_USER_POOLS.

image

I couldn't resume the project yet. But I will let you know if I still face problems.

Thank you

tannerabread commented 1 year ago

@phstc Sorry to be hasty on closing this issue. I'll be here if you need anything else. Thanks!

phstc commented 1 year ago

@tannerabread just tested it again with another app but with the same setup, and I'm still getting the same error.

image

export const getServerSideProps = async ({ req }) => {
  const SSR = withSSRContext({ req });

  try {
    const myInit = {
      headers: {
        Authorization: `Bearer ${(await SSR.Auth.currentSession())
          .getIdToken()
          .getJwtToken()}`,
      },
    };

    // works fine!
    // const r = await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/orders`, myInit);
    // console.log(await r.json());

    // this outputs
    console.log("before");
    // throws an error "No current user"
    const orders = await SSR.API.get("API", "orders", myInit);
    // this does not output, it fails on the line above
    console.log("after", orders);

    return {
      props: { orders },
    };
  } catch (e) {
    console.error(e);
    return {
      props: {},
    };
  }
};
tannerabread commented 1 year ago

Hi @phstc would you be able to provide a sample repo of this that I could test from?

didemkkaslan commented 1 year ago

@phstc I was finally able to get passed the "No current user" error.

It seems like the custom header in your amplify configure might be causing the issue. I did quite a bit of digging and logging and noticed that the error was actually being thrown from AuthClass and the more specific error was "Failed to get user from user pool"

I was able to get rid of the error by removing the entire custom_header section from Amplify.configure.

Personally, I am now getting 401/403 errors but I believe that is a different issue from how I set up this API.

Please let me know if this solves the issue for you as well

EDIT: Another note, it works successfully when I manually configure the project with the resources that I made through the CLI, so my 401/403 errors were definitely from some bad resource setup on my end.

My final configuration was as follows:

Amplify.configure({
  Auth: {
    region: "XX-XXXX-X",
    userPoolId: "XX-XXXX-X_abcd1234",
    userPoolWebClientId: "a1b2c3d4e5f6g7h8i9j0k1l2m3",
    identityPoolId: "XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab",
  },
  API: {
    endpoints: [
      {
        name: "apiName",
        endpoint: "https://12a3bcdefg.execute-api.us-east-1.amazonaws.com/dev/",
        region: "XX-XXXX-X"
      },
    ],
  },
  ssr: true,
});

This in turn allowed me to use API.get as expected in getServerSideProps:

export async function getServerSideProps({ req }) {
  const apiName = "apiName";
  const path = "items";

  const SSR = withSSRContext({ req });
  const response = await SSR.API.get(apiName, path);

  return {
    props: {
      response,
    },
  };
}

Removing the custom_header worked for mee <3

pix303 commented 1 year ago

Hi, same problem here with same stack (nextjs 13.1, REST, SSR, amplify + cognito)

The only way for working it's to set Authorization Bearer header in getServerSideProps grabbing jwt from context by Auth.currentSession(). But this involves apply this code every request. API-Gateway every endpoint is setted with Authorization COGNITO_USER_POOLS.

Is it possible in my configuration i haven't Identity Pool Id? No federation identity setted here. Is it neccessary?

phstc commented 1 year ago

@didemkkaslan if I remove custom_header then nothing works, I get 401 from APIGW.

@tannerabread would you be open to pair up on this?. It's going to be much quicker for me to share my screen than set up a new project. Please, let me know if that works for you.

phstc commented 1 year ago

Another behavior that's very strange. If I click on a link, from another website, for example, I click on localhost:3000/something from example.com, it fails for the first time, getServerSideProps can't authorize the request, then, if I manually enter that URL, it works just fine (with my custom header, I still have the initial issue).

blackmouth commented 1 year ago

I recently upgraded my Next11 project to use WEB_COMPUTE and I ran into problems with the SSR context working locally but not when deployed. One major problem was receiving 'No current user'. The issue finally resolved when I changed the SSRContext variable from a const to a let.

Changing

  const SSR = withSSRContext({ req });

to

  let SSR = withSSRContext({ req });

  //Calling Amplify classes as just examples.
  SSR.Auth.currentAuthenticatedUser()
  SSR.API......
  SSR.Datastore.....

This change resolved an issue with Auth and other SSR Amplify classes when migrating from Next11 to Next12|13 and changing the Amplify platform to WEB_COMPUTE.

Here's my current working dependencies

"dependencies": {    
  "@aws-amplify/ui-react": "4.3.7",
  "@aws-amplify/core": "5.0.11",
  "@aws-amplify/geo":"2.0.11",
  "aws-amplify": "5.0.11",
  "next": "^12.3.4",
  "react": "17.0.2",
  "react-dom": "17.0.2",
}
phstc commented 1 year ago

@blackmouth I get TypeError: SSR.Auth.function is not a function my dependencies:

    "@aws-amplify/auth": "^5.1.2",
    "@aws-amplify/ui-react": "^4.3.1",
    "aws-amplify": "^5.0.11",
    "next": "13.1.1",
blackmouth commented 1 year ago

Sorry for the confusion, I updated my comment.

weisisheng commented 1 year ago

Is this related to some cookies setting which must be explicitly placed in aws-exports.json? Hours and hours of search only vaguely makes reference.

nadetastic commented 1 year ago

Hi @phstc following up here,

I also haven't been able to reproduce your issue and I get the expected result when configuring similar to what you have. However one thing l'd like to confirm - on your Authorizer for API Gateway, are you specifying the Token Source?

APIGWY Authorizer

@weisisheng for your question, you shouldn't have to modify cookie config from aws-exports

phstc commented 1 year ago

Hi @nadetastic

Yes, I am.

image

BTW this works fine on the client side. The issue is when running it through getServerSideProps. Also, this works on getServerSideProps when I use fetch (example here).

nadetastic commented 1 year ago

Thanks for the confirmation @phstc.

The only thing I can think of now is an issue specific to CDK - I tested by manually adding via the console and also using the Amplify CLI I am able to get things working.

Could you share a simplified sample of your CDK stack? In addition to trying reproduce this with your CDK stack, I'm also hoping you can confirm if all your resources are created with the CDK - Cognito, API, Lambda etc - and are just hooking the resources to you Amplify app.

phstc commented 1 year ago

@tannerabread @nadetastic I was able to create a repo cognito-amplify-cdk-10290 with the code to reproduce the problem.

I added deploy instructions in the README to configure the sample code. Please, let me know if that works for you.

Once you run it locally npm run dev and then open http://localhost:3000, there will be two links:

image

One example is working using fetch discussed previously, and the other one is the broken one using SSR.API.

nadetastic commented 1 year ago

@phstc thank you very much for sharing the sample app! I will follow up soon

nadetastic commented 1 year ago

@phstc I was able to get this working as seen on my fork of your repo - https://github.com/nadetastic/cognito-amplify-cdk-10290/commit/0efcc007dfc35fccc408cff9173eb93caca12b37

In short, updated from using the custom header within amplifyConfig.API to setting it within the headers right before calling SSR.API.get(). This was pointed out before but you mentioned that you were still having issues after removing the custom header, but I'd like to note that I created new resources using the CDK stack you provided, so it doesn't seem to be an issue with the CDK definition (backend looks unchanged in my repo, but I did modify it as you stated in the README just didn't push the changes).

Are you able to try with the changes I have added? - https://github.com/nadetastic/cognito-amplify-cdk-10290

phstc commented 1 year ago

Hi @nadetastic thank you so much for looking into this. I really appreciate it ❤️

Are you able to try with the changes I have added?

Yes, with the changes it works, but 👇

you mentioned that you were still having issues

Sorry for not clarifying that. If I remove the custom header, then I need to add myInit to all requests, SSR or not. I added an example here.

One of the reasons for custom_header was to enforce and simplify (not having to explicitly add myInit) other requests, SSR or not.

But apparently, that option does not work with SSR. Is that expected or a bug?


There's still another issue with both approaches, custom-header or not.

If you click here http://localhost:3000/works-ssr (from the GitHub comment) it fails with No Current User, but if you copy http://localhost:3000/works-ssr and paste that into your browser, it works. If you click from an external link (Gmail, GitHub, Slack, etc.), it fails, but if you enter the URL directly or if it's a link within localhost (or whatever app domain), it works.

pix303 commented 1 year ago

Hi, i'm facing same problem as @phstc and only solution found until now it's:

in amplify.config.ts use a try catch so i can keep setting for all requests (SSR and client)

API: {
    endpoints: [
      {
        name: 'BE-API',
        endpoint: process.env.NEXT_PUBLIC_APIGATEWAY_URL,
        region: process.env.NEXT_PUBLIC_REGION,
        custom_header: async () => {
          try {
            const session = await Auth.currentSession();
            const token = session.getIdToken();
            const jwt = token.getJwtToken();

            return {
              Authorization: `Bearer ${jwt}`,
            };
          } catch (error) {
            return {};
          }
        },
      },
    ],
  },
  ssr: true,

for every fetch in getServerSideProps it must build headers as in config

...
const { Auth, API } = withSSRContext(context);
const session = await Auth.currentSession();
const token = session.getIdToken();
const jwt = token.getJwtToken();
const headers = { Authorization: `Bearer ${jwt}` };
result = await API.get('BE-API', '/endpoint', { headers });
return {props: result}

for client request (usually mutation as post, put) API works as expected

  const submit = useCallback(
    async (data) => {
      try {
          await API.post('BE-API', '/endopoint', { body: user });
      } catch (err: any) {
        // show error
      }
    },
    [],
  );

The error returned in SSR is "no current user" (as i recall). Cognito seems to be configured correctly.

nadetastic commented 1 year ago

@phstc after some discussions with the team on this, I'm marking this as a bug. Thank you for all your time and feedback on this - truly appreciated. Let me know if you have any other questions

phstc commented 1 year ago

@nadetastic thank you! Just confirming, there are two issues:

  1. SSR does not work with custom_header
  2. External links don't not work with SSR (there's still another issue)
cwomack commented 10 months ago

With the release of the latest major version of Amplify (aws-amplify@>6), this issue should now be resolved! Please refer to our release announcement, migration guide, and documentation for more information.