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.44k stars 2.13k forks source link

Cannot authorize with amplify aws and NextJS 14 app router. #13956

Closed cong-tri closed 1 week ago

cong-tri commented 4 weeks ago

Before opening, please confirm:

JavaScript Framework

React, Next.js

Amplify APIs

Authentication, Storage

Amplify Version

v6

Amplify Categories

auth, storage, hosting

Backend

None

Environment information

System: OS: Windows 10 10.0.19045 CPU: (12) x64 Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz Memory: 19.96 GB / 31.82 GB Binaries: Node: 20.15.0 - C:\Program Files\nodejs\node.EXE npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD Browsers: Internet Explorer: 11.0.19041.1566 npmPackages: @ampproject/toolbox-optimizer: undefined () @ant-design/icons: ^5.3.7 => 5.3.7 @ant-design/nextjs-registry: ^1.0.0 => 1.0.0 @aws-amplify/adapter-nextjs: ^1.2.11 => 1.2.11 @aws-amplify/adapter-nextjs/api: undefined () @aws-amplify/adapter-nextjs/data: undefined () @babel/core: undefined () @babel/runtime: 7.22.5 @edge-runtime/cookies: 4.1.1 @edge-runtime/ponyfill: 2.4.2 @edge-runtime/primitives: 4.1.0 @hapi/accept: undefined () @mswjs/interceptors: undefined () @napi-rs/triples: undefined () @next/font: undefined () @opentelemetry/api: undefined () @tanstack/query-codemods: undefined () @tanstack/react-query: ^5.50.1 => 5.51.1 @types/node: ^20 => 20.14.2 @types/react: ^18 => 18.3.3 @types/react-dom: ^18 => 18.3.0 @vercel/nft: undefined () @vercel/og: 0.6.2 acorn: undefined () amphtml-validator: undefined () anser: undefined () antd: ^5.18.1 => 5.18.1 arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () aws-amplify: ^6.4.4 => 6.4.4 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/data: undefined () aws-amplify/data/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 commander: 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 () css.escape: undefined () data-uri-to-buffer: undefined () debug: undefined () devalue: undefined () domain-browser: undefined () edge-runtime: undefined () eslint: ^8 => 8.57.0 eslint-config-next: 14.2.4 => 14.2.4 events: undefined () find-cache-dir: undefined () find-up: undefined () fresh: undefined () get-orientation: undefined () glob: undefined () gzip-size: undefined () http-proxy: undefined () http-proxy-agent: undefined () https-browserify: undefined () https-proxy-agent: 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-runner: undefined () loader-utils: undefined () lodash.curry: undefined () lru-cache: undefined () mini-css-extract-plugin: undefined () moment: ^2.30.1 => 2.30.1 nanoid: undefined () native-url: undefined () neo-async: undefined () next: 14.2.4 => 14.2.4 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () picomatch: undefined () platform: undefined () postcss: ^8 => 8.4.38 (8.4.31) 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 => 18.3.1 react-builtin: undefined () react-dom: ^18 => 18.3.1 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () react-is: 18.2.0 react-refresh: 0.12.0 react-server-dom-turbopack-builtin: undefined () react-server-dom-turbopack-experimental-builtin: undefined () react-server-dom-webpack-builtin: undefined () react-server-dom-webpack-experimental-builtin: undefined () recharts: ^2.12.7 => 2.12.7 regenerator-runtime: 0.13.4 sass-loader: undefined () scheduler-builtin: undefined () scheduler-experimental-builtin: undefined () schema-utils: undefined () semver: undefined () send: undefined () server-only: 0.0.1 setimmediate: undefined () shell-quote: undefined () socket.io: ^4.7.5 => 4.7.5 socket.io-client: ^4.7.5 => 4.7.5 source-map: undefined () source-map08: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () superstruct: undefined () tailwindcss: ^3.4.1 => 3.4.4 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () typescript: ^5 => 5.4.5 typescript-cookie: ^1.0.6 => 1.0.6 ua-parser-js: undefined () unistore: undefined () util: undefined () uuid: ^10.0.0 => 10.0.0 (9.0.1) vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: undefined () npmGlobalPackages: @aws-amplify/cli: 12.12.3 npm: 10.8.2 typescript: 5.5.4

Describe the bug

I have a project using nextjs 14 app router and amplify aws. I cannot handle login when i have called signIn and after called confirmSignIn to entered the challengeResponse OTP code to authorized. But cannot work for me, it just return error "Unable to get user session following successful sign-in.". I have set up configure Amplify at client side with ssr = true and server side: runWithAmplifyServerContext with createServerRunner. I don't know where the bug at, so i think may be at set up domain for cookie or something else can be missing.

Expected behavior

I want to code can work because i have this project for a long times Sorry about the bug because i have studied amplify aws just a little. Please help me.

Reproduction steps

  1. npm install aws-amplify , aws-amplify/adapter-nextjs
  2. configure amplify in nextjs:
    • client side with ssr = true
    • server side with runWithAmplifyServerContext
  3. call signIn and confirmSignIn in component sign-in-form

Code Snippet

// client side file config: "use client";

import { Amplify } from "aws-amplify"; import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito"; import { CookieStorage } from "aws-amplify/utils"; import { getAmplifyConfig } from "../../amplify-config";

const config = getAmplifyConfig();

Amplify.configure(config, { // ssr: true, });

cognitoUserPoolsTokenProvider.setKeyValueStorage( new CookieStorage({ domain: http://${process.env.NEXT_PUBLIC_DOMAIN_NOT_SECURE}, path: ${process.env.NEXT_PUBLIC_BASE_PATH}, secure: false, // sameSite: "strict", }) );

export default function ConfigureAmplifyClientSide() { return null; } I have called this component at the top children in layout.tsx

// server side file config : import { createServerRunner } from "@aws-amplify/adapter-nextjs"; import { getAmplifyConfig } from "../../amplify-config";

const config = getAmplifyConfig();

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

// sign in component: /* @format /

"use client";

import React, { useEffect, useState } from "react";

import { useRouter } from "next/navigation";

import { App, Button, Form, Input, Modal } from "antd"; import Title from "antd/es/typography/Title"; import Typography from "antd/es/typography/Typography";

import { AuthError, SignInOutput, confirmSignIn, signIn, } from "aws-amplify/auth";

import { v4 as uuidv4 } from "uuid";

type Account = { username: string; password: string; };

function SignInForm() { const [isModalOpen, setIsModalOpen] = useState(false); const [isModal, setIsModal] = useState(false); const [valid, setValid] = useState(false); const [uuid, setUUID] = useState(); const [account, setAccount] = useState(); const [timeLeft, setTimeLeft] = useState(30); const [output, setOutPut] = useState();

const { message } = App.useApp();

const [form] = Form.useForm();

const router = useRouter();

const errorException = "[DUPLICATED_DEVICE]"; // a handle error custom

useEffect(() => { if (isModalOpen !== true) return;

if (timeLeft === 0) return;
const timerId = setInterval(() => {
  setTimeLeft(timeLeft - 1);
}, 1000);

return () => clearInterval(timerId);

}, [timeLeft, isModalOpen]);

const onFinish = async ({ username, password, }: { username: string; password: string; }) => { const myuuid = uuidv4();

setUUID(myuuid);

setAccount({ username, password });

try {
  const response = await signIn({
    username,
    password,
    options: {
      clientMetadata: {
        uuid: myuuid,
      },
    },
  });
  console.log(response);

  if (response.isSignedIn === false) setIsModalOpen(true);
  else {
    message.success("Login Successfully", 2, () => {
      router.refresh();
      router.push("/public-portal/user");
    });
  }
} catch (error) {
  handleThrowErrorMessage(error, errorException);
}

};

const handleReturnOutputSignin = async () => { try { const output = await signIn({ username: account?.username ?? "", password: account?.password, options: { clientMetadata: { uuid: uuid ?? "", mfaMethod: "EMAIL", }, }, }); return output; } catch (error) { handleThrowErrorMessage(error); } };

const handleContinueToSignin = async () => { try { const output = await handleReturnOutputSignin(); console.log("output >>", output);

  setOutPut(output);
  if (output?.nextStep.signInStep === "CONFIRM_SIGN_IN_WITH_SMS_CODE")
    setIsModalOpen(true);
} catch (error) {
  handleThrowErrorMessage(error);
}

};

const handleSignInNextSteps = async ({ otpCode }: { otpCode: string }) => { try { if (!output) await handleReturnOutputSignin();

  if (
    output?.isSignedIn === false &&
    output?.nextStep.signInStep === "CONFIRM_SIGN_IN_WITH_SMS_CODE"
  ) {
    await confirmSignIn({ challengeResponse: otpCode, options: {
      userAttributes: {}
    } });
    setValid(true);

    message.success("Login Successfully", 2, () => {
      router.refresh();
      router.push("/user");
    });
  }
} catch (error) {
  handleThrowErrorMessage(error);
}

};

const handleResendOTPCode = async () => { try { await handleReturnOutputSignin(); setTimeLeft(30); } catch (error) { handleThrowErrorMessage(error); } };

const handleThrowErrorMessage = (error: any, errorKey: string = "") => { console.error(error);

if (error instanceof AuthError) {
  if (errorKey != errorException) {
    message.error(error.message, 5);
  }
}

if (error.message.includes(errorKey)) {
  setIsModal(true);
}

};

return ( <> <Form form={form} name="basic" onFinish={onFinish} autoComplete="off" style={{ width: 800 }}

Username:

<Form.Item name="username" style={{ width: "100%" }} rules={[{ required: true, message: "Please input your username!" }]} initialValue={"+84326034561"}

</Form.Item>

    <Title level={3}>Password:</Title>
    <Form.Item<Account>
      name="password"
      rules={[{ required: true, message: "Please input your password!" }]}
      initialValue={"Tri@2024"}
    >
      <Input.Password placeholder="Your password" size="large" />
    </Form.Item>

    <Form.Item>
      <Button htmlType="submit" type="primary" size="large">
        Submit
      </Button>
    </Form.Item>
  </Form>

  <Modal
    title={<Title level={3}>Sign In Failed</Title>}
    centered
    width={300}
    open={isModal}
    onOk={() => setIsModal(true)}
    onCancel={() => setIsModal(false)}
    footer={
      <>
        <Button
          htmlType="button"
          type="primary"
          danger
          onClick={() => setIsModal(false)}
        >
          Close
        </Button>
        <Button
          htmlType="button"
          type="primary"
          onClick={() => handleContinueToSignin()}
        >
          CONTINUE
        </Button>
      </>
    }
  >
    <Typography>
      You have already logged in on another device. Please sign out of the
      other device or CONTINUE
    </Typography>
  </Modal>

  <Modal
    title={<Title type="secondary">Verification</Title>}
    centered
    width={350}
    open={isModalOpen}
    onOk={() => setIsModalOpen(true)}
    onCancel={() => {
      if (!valid) {
        message.error("Please submit the OTP code to close");
        return;
      }
      setIsModalOpen(false);
    }}
    footer={
      <>
        <Button
          htmlType="button"
          type="primary"
          danger
          disabled={!valid}
          onClick={() => setIsModalOpen(false)}
        >
          Close
        </Button>
      </>
    }
  >
    <Form
      form={form}
      name="otp"
      onFinish={handleSignInNextSteps}
      autoComplete="off"
      style={{ width: "100%" }}
    >
      <Title level={4}>
        Please, enter the verification code that has been sent to your email
      </Title>
      <Form.Item
        name="otpCode"
        hasFeedback
        validateStatus="success"
        rules={[
          {
            required: true,
            message: "Please enter the OTP code before submit!",
          },
        ]}
      >
        <Input.OTP />
      </Form.Item>
      <Form.Item>
        <Button
          htmlType="submit"
          className="w-full"
          type="primary"
          size="large"
        >
          Submit
        </Button>
        <Button
          htmlType="button"
          className="w-full mt-5"
          type="default"
          size="large"
          disabled={timeLeft == 0 ? false : true}
          onClick={() => handleResendOTPCode()}
        >
          Resend OTP Code ({timeLeft ?? ""})
        </Button>
      </Form.Item>
    </Form>
  </Modal>
</>

); }

export default SignInForm

Log output

image

aws-exports.js

// .env file: NEXT_PUBLIC_BASE_PATH=/public-portal NEXT_PUBLIC_AWS_COGNITO_DOMAIN=https://cognito-idp.ap-southeast-1.amazonaws.com/ap-southeast-1_xxxxxx NEXT_PUBLIC_AWS_COGNITO_IDENTITY_POOL_ID=ap-southeast-1:xxxxxx NEXT_PUBLIC_AWS_COGNITO_USER_POOL_ID=ap-southeast-1_xxxxxxxx NEXT_PUBLIC_AWS_COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_DOMAIN_NOT_SECURE=192.xxx.x.xxx:xxxx # for not secure NEXT_PUBLIC_DOMAIN_SECURE=dev.xxxxx.com:xxxx # for secure

export const getAmplifyConfig = (domain: string = ${process.env.NEXT_PUBLIC_DOMAIN_NOT_SECURE}): ResourcesConfig => { const amplifyConfig: ResourcesConfig = { Auth: { Cognito: { // identityPoolId: process.env.NEXT_PUBLIC_AWS_COGNITO_IDENTITY_POOL_ID ?? '', userPoolId: process.env.AWS_COGNITO_USER_POOL_ID ?? "", userPoolClientId: process.env.AWS_COGNITO_CLIENT_ID ?? "", loginWith: { oauth: { domain: process.env.AWS_COGNITO_DOMAIN ?? "", scopes: [ "email", "openid", "profile", "aws.cognito.signin.user.admin", ], redirectSignIn: [http://${domain}${process.env.NEXT_PUBLIC_BASE_PATH}/signin], redirectSignOut: [http://${domain}${process.env.NEXT_PUBLIC_BASE_PATH}/sign-out], responseType: "token", }, email: false, username: true, phone: false, } },

  /*
  API: {
    endpoints: [
      {
        name: 'talent',
        endpoint: process.env.NEXT_PUBLIC_AWS_TALENT_API_DOMAIN,
        custom_header: authen_header,
      },
    ],
  },
  */
},

};

return amplifyConfig; };

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

cwomack commented 3 weeks ago

Hello, @cong-tri đź‘‹. There's a lot of details in this issue and we might need to break down to determine the root cause. To start, can you let me know what documentation you're following or where you started with this?

One issue might be that there's some discrepancies on how you're configuring MFA (placing it within the scopes for OAuth, but then marking it as false for email for example. Can you clarify what you're looking to implement for your authentication flows?

Second thing that stands out is the use of http:// outside of your environment variables as well as setting secure: false within your custom CookieStorage. If you change the cookie attributes on the client side, they may be overwritten by the server side attributes (we currently do don't support configuring attributes on the server side). You should only have to use the ssr: true flag rather than needing to create a customer CookieStorage yourself.

Is there any chance you could create a sample repo to share that we can make suggestions to the most recent code being used and for reproducing the exceptions you're getting?

cong-tri commented 3 weeks ago

For starting, i am following aws amplify nextjs documentation section "https://docs.amplify.aws/nextjs/build-a-backend/server-side-rendering/" for my task. Step 1: I made a component to login to the website. In that component I will call the signIn function to pass in the username and password. Step 2: And if I get the "Duplicate Device" error, I will call the signIn function again and pass an additional parameter mfaMethod: "Email" to the clientMetadata of options so that I can email the user an OTP code for authentication. Step 3: Finally, I will call the confirmSignIn function to pass the challengeResponse: OTP code; to complete the final authentication screen to log in. But I'm having a problem with the last step: when I call the confirmSignIn function to pass in the challegeResponse: OTP code, the browser reports the error "Unable to get user session after successful sign-in.". I had to stop at this step for a while. I'm willing to try many ways to solve the problem so please give me suggestions.

cong-tri commented 3 weeks ago

If i setup : Amplify.configure(config, { // ssr: true, }); // cognitoUserPoolsTokenProvider.setKeyValueStorage( // new CookieStorage({ // domain: http://${process.env.DOMAIN_NOT_SECURE}, // path: ${process.env.BASE_PATH}, // secure: false, // // sameSite: "strict", // }) // ); it could be signed in successfully but it stored some parameters from CognitoIdentityServiceProvider return at localStorage It is not at cookie storage.

And if i enable ssr: true it could not work for me. It just browser return a error "Unable to get user session after successful sign-in." in the last step.

cwomack commented 3 weeks ago

@cong-tri, are you able to also share what the network response form Cognito is when you get that initial 400 status code and the confirmSignIn() call? And if you're building a sample app from the docs, any chance you can share a sample repo that we could test with as well?

cwomack commented 2 weeks ago

@cong-tri, wanted to check in again and see if you saw the above comment and questions.

cwomack commented 1 week ago

Closing this issue as we have not heard back from you. If you are still experiencing this, please feel free to reply back and provide any information previously requested and we'd be happy to re-open the issue.

Thank you!