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

NextJS + Amplify V6 signIn command returns `User needs to be authenticated to call this API` even when its runned in client side with SSR enabled #12866

Closed muratcali closed 6 months ago

muratcali commented 8 months ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

Authentication

Amplify Version

v6

Amplify Categories

No response

Backend

Amplify CLI

Environment information

``` System: OS: macOS 14.2.1 CPU: (8) arm64 Apple M1 Memory: 698.50 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.10.0 - /usr/local/bin/node npm: 10.2.3 - /usr/local/bin/npm Browsers: Chrome: 120.0.6099.234 Edge: 120.0.2210.144 Safari: 17.2.1 npmPackages: @ampproject/toolbox-optimizer: undefined () @aws-amplify/adapter-nextjs: ^1.0.9 => 1.0.9 @aws-amplify/adapter-nextjs/api: undefined () @aws-amplify/adapter-nextjs/data: undefined () @aws-amplify/ui-react: ^6.0.7 => 6.0.7 @aws-amplify/ui-react-internal: undefined () @babel/core: undefined () @babel/runtime: 7.22.5 @edge-runtime/cookies: 4.0.2 @edge-runtime/ponyfill: 2.4.1 @edge-runtime/primitives: 4.0.2 @formatjs/intl-localematcher: ^0.5.2 => 0.5.2 @hapi/accept: undefined () @mswjs/interceptors: undefined () @napi-rs/triples: undefined () @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @radix-ui/react-alert-dialog: ^1.0.5 => 1.0.5 @radix-ui/react-icons: ^1.3.0 => 1.3.0 @radix-ui/react-label: ^2.0.2 => 2.0.2 @radix-ui/react-navigation-menu: ^1.1.4 => 1.1.4 @radix-ui/react-slot: ^1.0.2 => 1.0.2 (1.0.0) @segment/ajv-human-errors: undefined () @types/negotiator: ^0.6.3 => 0.6.3 @types/node: ^20 => 20.9.1 @types/react: ^18 => 18.2.37 @types/react-dom: ^18 => 18.2.15 @vercel/nft: undefined () @vercel/og: 0.5.15 acorn: undefined () amphtml-validator: undefined () anser: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () autoprefixer: ^10.0.1 => 10.4.16 aws-amplify: ^6.0.9 => 6.0.9 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/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 () class-variance-authority: ^0.7.0 => 0.7.0 cli-select: undefined () client-only: 0.0.1 clsx: ^2.0.0 => 2.0.0 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.53.0 eslint-config-next: 14.0.3 => 14.0.3 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 () micromatch: undefined () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () negotiator: ^0.6.3 => 0.6.3 neo-async: undefined () next: 14.0.3 => 14.0.3 next-auth: ^4.24.5 => 4.24.5 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss: ^8 => 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.2.0 (16.14.0) react-builtin: undefined () react-code-input: ^3.10.1 => 3.10.1 react-dom: ^18 => 18.2.0 (16.14.0) 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 () 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 () source-map: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () superstruct: undefined () tailwind-merge: ^2.1.0 => 2.1.0 tailwindcss: ^3.3.0 => 3.3.5 tailwindcss-animate: ^1.0.7 => 1.0.7 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () typescript: ^5 => 5.2.2 ua-parser-js: undefined () unistore: undefined () util: undefined () vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: undefined () npmGlobalPackages: @aws-amplify/cli: 10.7.1 corepack: 0.22.0 eas-cli: 3.4.1 expo-cli: 6.1.0 npm: 10.2.3 ```

Describe the bug

When I run the signIn command imported from import { signIn, type SignInInput } from "aws-amplify/auth"; in a file marked with use client it returns te following error: User needs to be authenticated to call this API. Ive also enables SSR inside Amplify.configure. I really can't figure out how to fix this problem for days now. Ive followed every step in the official docs. Thanks in advance.

Expected behavior

The command needs to sign me in.

Reproduction steps

  1. Running from a
    the command signIn({ username, password })
  2. It returns: User needs to be authenticated to call this API even when its runned with "use client"

Code Snippet

Login.tsx:


"use client";

import {
    AlertDialog,
    AlertDialogAction,
    AlertDialogCancel,
    AlertDialogContent,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
    AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "../button";
import { ExclamationTriangleIcon, ReloadIcon } from "@radix-ui/react-icons";
import { FormEvent, useState } from "react";
import { Alert, AlertDescription } from "../alert";
import handleSignIn from "@/hooks/useSignIn";

export default function LoginModal(lang: any) {
    const [loading, setLoading] = useState<boolean>(false);
    const [email, setEmail] = useState<string>("");
    const [password, setPassword] = useState<string>("");
    const [signinResponse, setSigninResponse] = useState<any[]>([false, 0, ""]);

    const onSignIn = async (e: any) => {
        e.preventDefault();
        setLoading(true);
        setSigninResponse([false, 0, ""]);
        try {
            const response = await handleSignIn({
                username: email,
                password: password,
            });
            if (response[0]) {
                console.log(response);
            } else {
                setSigninResponse([true, 0, response[2]]);
            }
        } catch (error: any) {
            console.error("Fout bij inloggen:", error);
            setSigninResponse([true, 0, error.message]);
        } finally {
            setLoading(false);
        }
    };

    return (
        <AlertDialog>
            <AlertDialogTrigger className="hover:bg-accent hover:text-primary text-sm">
                Inloggen
            </AlertDialogTrigger>
            <AlertDialogContent className="bg-white">
                <AlertDialogHeader className="border-b pb-3">
                    <AlertDialogTitle className="text-xl">
                        Welkom terug!
                    </AlertDialogTitle>
                </AlertDialogHeader>
                <form className="w-full h-full grid gap-2" onSubmit={onSignIn}>
                    <div className="grid w-full max-w-sm items-center gap-1.5">
                        <Label htmlFor="email">Emailadres</Label>
                        <Input
                            type="email"
                            id="email"
                            className={
                                signinResponse[1] === 1 ? "!border-alert" : ""
                            }
                            onChange={(e: FormEvent<HTMLInputElement>) =>
                                setEmail(e.currentTarget.value)
                            }
                        />
                    </div>
                    <div className="grid w-full max-w-sm items-center gap-1.5">
                        <Label htmlFor="password">Wachtwoord</Label>
                        <Input
                            type="password"
                            id="password"
                            className={
                                signinResponse[1] === 2 ? "!border-alert" : ""
                            }
                            onChange={(e: FormEvent<HTMLInputElement>) =>
                                setPassword(e.currentTarget.value)
                            }
                        />
                    </div>
                    <AlertDialogFooter className="flex flex-col">
                        {signinResponse[2] !== "" && (
                            <Alert variant="destructive" className="mb-2">
                                <ExclamationTriangleIcon className="h-4 w-4" />
                                <AlertDescription className="pt-1">
                                    {signinResponse[2]}
                                </AlertDescription>
                            </Alert>
                        )}
                        <span className="text-sm text-primary hover:text-secondary hover:cursor-pointer">
                            Wachtwoord vergeten?
                        </span>
                        <span className="text-sm mt-2 text-primary hover:text-secondary hover:cursor-pointer">
                            Geen account?
                        </span>
                    </AlertDialogFooter>
                    {loading ? (
                        <Button disabled>
                            <ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
                            Inloggen
                        </Button>
                    ) : (
                        <Button variant={"default"} type="submit">
                            Inloggen
                        </Button>
                    )}
                </form>
                <AlertDialogCancel className="text-xs hover:underline cursor-pointer">
                    Annuleren
                </AlertDialogCancel>
            </AlertDialogContent>
        </AlertDialog>
    );
}

useSignIn.ts:


"use client";
import { signIn, type SignInInput } from "aws-amplify/auth";

export default async function handleSignIn({
    username,
    password,
}: SignInInput) {
    try {
        const { isSignedIn, nextStep } = await signIn({ username, password });
        console.log(isSignedIn, nextStep);
        return [true, 0, ""];
    } catch (error: any) {
        return [false, 0, error.message.toString()];
    }
}

layout.tsx:


import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import "./globals.css";
import { site } from "@/configuration";
import TopNavigation from "@/components/nav/site/topnav";
import clsx from "clsx";
import { getDictionary } from "@/hooks/useDictionaries";
import RootLayoutThatConfiguresAmplifyOnTheClient from "@/utils/amplifyConfigureOnClient";

type LayoutProps = {
    params: { lang: string };
    children: React.ReactNode;
};

const font = Poppins({
    variable: "--font-poppins",
    weight: "400",
    subsets: ["latin"],
});

export const metadata: Metadata = {
    title: site.name,
    description: site.description,
};

export default async function RootLayout({ children, params }: LayoutProps) {
    const dict = await getDictionary(params.lang);

    return (
        <html lang={params.lang}>
            <RootLayoutThatConfiguresAmplifyOnTheClient>
                <body
                    className={clsx(
                        font.className,
                        "h-screen w-screen overflow-hidden"
                    )}
                >
                    <TopNavigation lang={dict} />
                    <div className="h-full overflow-auto mx-auto max-w-screen">
                        {`Locale: ${params.lang}`}
                        {children}
                    </div>
                </body>
            </RootLayoutThatConfiguresAmplifyOnTheClient>
        </html>
    );
}

amplifyConfigureOnClient.ts:


"use client";

import config from "@/amplifyconfiguration.json";
import { Amplify } from "aws-amplify";

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

export default function RootLayoutThatConfiguresAmplifyOnTheClient({
    children,
}: {
    children: React.ReactNode;
}) {
    return children;
}

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

israx commented 8 months ago

hello @muratcali . Sorry to hear you are experiencing issues with the library. We will reproduce this issue and will follow up with any updates

israx commented 8 months ago

hey @muratcali . I'm not able to reproduce the issue, the sign-in API is working on my end. The sign-in API shouldn't return the User needs to be authenticated message but instead it would throw a UserAlreadyAuthenticated exception if it detects cookies stored.

Could you clarify whether the API is returning or throwing an error ?

cwomack commented 8 months ago

Hello, @muratcali šŸ‘‹. I couldn't reproduce this either when following the docs for using Auth and the signIn() API for an SSR app. Have another question for you in addition to what @israx asked in the above comment to see if we can unblock you.

Can you share a little more about what you're trying to do on the server side? With you setting ssr to true in your call of Amplify.configure(config, { ssr: true }), you may need to import additional functions such as createKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider, createAWSCredentialsAndIdentityIdProvider, and unWithAmplifyServerContext if you're attempting to call any of these Auth API's on the server side. I just didn't see them imported anywhere in the code snippets provided.

fimbres commented 8 months ago

Hey, I'm facing a similar issue using aws-amplify "^6.0.12" and nextjs "14.1.0" with app router, in client side everything works as expected, but then hitting my middleware or any backend route with a request shows that the authenticated user is not defined. So basically I'm able to create an account and sign in from my nextjs app, but if I hit a backend route or the middleware.ts file it seems that I'm not authenticated after all, or the session is not stored correctly. (Doing another sign in returns the error in client side: UserAlreadyAuthenticatedException: There is already a signed in user.)

Error in middleware/backend routes:

[NotAuthorizedException: Unauthenticated access is not supported for this identity pool.] {
  name: 'NotAuthorizedException',
  $metadata: {
  attempts: 1,
  httpStatusCode: 400,
  requestId: '5bd172d7-26ff-408b-a5b5-2925a9ebb64b',
  extendedRequestId: undefined,
  cfId: undefined
}

I followed the updated documentation, this is my middleware.ts file:

import { NextRequest, NextResponse } from "next/server";
import { fetchAuthSession } from "aws-amplify/auth/server";

import { runWithAmplifyServerContext } from "@/providers/amplify-server";

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

  const authenticated = await runWithAmplifyServerContext({
    nextServerContext: { request, response },
    operation: async (contextSpec) => {
      try {
        const session = await fetchAuthSession(contextSpec);
        return !!session.tokens;
      } catch (error) {
        console.log(error);
        return false;
      }
    },
  });

  if (authenticated) {
    return response;
  }

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

export const config = {
  matcher: "/dashboard/:path*",
};

This is my login function: (being called in a client page with "use client" statement)

import {
  signIn,
  type SignInInput,
} from "aws-amplify/auth"

async function handleSignIn({ username, password }: SignInInput) {
  try {
    const response = await signIn({ username, password })

    return response
  } catch (error: any) {
    console.error(error)
    toast(error?.message || "Something went wrong, please try again.")

    return null
  }
}

And this is how I'm calling this handleSignIn function in my login page:

import { handleSignIn } from "@/lib/auth"

...

const onSubmit = async (values: FormType) => {
    const response = await handleSignIn(values);

    if(!response) return;

    router.push('/dashboard'); //doing this is hitting the middleware, but is returning me to the same login page because for the middleware I'm not authenticated.
  }

...

And lastly, this is how I'm setting up AWS Amplify in both server and client sides:

// server config

import { cookies } from "next/headers";
import { Amplify } from "aws-amplify"
import { fetchAuthSession } from "aws-amplify/auth";
import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito"
import { defaultStorage } from "aws-amplify/utils"
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
import { createServerRunner } from "@aws-amplify/adapter-nextjs";

import config from "@/aws-exports"

Amplify.configure(config, {
  ssr: true,
})
cognitoUserPoolsTokenProvider.setKeyValueStorage(defaultStorage)

export const serverClient = generateServerClientUsingCookies({
  config,
  cookies,
});

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

"use client";

import { Amplify } from "aws-amplify"
import { defaultStorage } from "aws-amplify/utils";
import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito";

import config from "@/aws-exports"

Amplify.configure(config, {
  ssr: true,
})
cognitoUserPoolsTokenProvider.setKeyValueStorage(defaultStorage)

export default function ConfigureAmplifyClientSide() {
  return null;
}

If you need more information feel free to ping me.

fimbres commented 8 months ago

Hey guys my bad šŸ˜…. I just find out that this line: cognitoUserPoolsTokenProvider.setKeyValueStorage(defaultStorage)

Was the cause of the issue for me, I saw it here in the docs so I never thought that line will be causing issues in my app lol. Let me know if that's an expected behavior. But I believe it's not.

Now everything works great!

muratcali commented 8 months ago

Thanks for your help and message.

@israx the error im receiving is UserUnAuthenticatedException: User needs to be authenticated to call this API.

@cwomack Here are some code snippets from my serverside code:

Middleware.tsx

import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import { site } from "./configuration";
import { NextRequest, NextResponse } from "next/server";
import { runWithAmplifyServerContext } from "@/utils/amplifyServerUtils";
import { fetchAuthSession } from "aws-amplify/auth/server";

//prettier-ignore
const { languages: { locales } } = site;
//prettier-ignore
const { languages: { defaultLocale } } = site;

function getLocale(request: NextRequest) {
    let headers: any = {
        "accept-language": request.headers.get("accept-language"),
    };
    const languages = new Negotiator({ headers }).languages();
    return match(languages, locales, defaultLocale);
}

export async function middleware(request: NextRequest) {
    const response = NextResponse.next();
    const { pathname } = request.nextUrl;
    const authenticated = await runWithAmplifyServerContext({
        nextServerContext: { request, response },
        operation: async (contextSpec) => {
            try {
                const session = await fetchAuthSession(contextSpec);
                console.log(session);
                return session.tokens !== undefined;
            } catch (error) {
                console.log(error);
                return false;
            }
        },
    });
    const locale = getLocale(request);
    const pathnameHasLocale = locales.some(
        (locale) =>
            pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
    );

    if (pathname.includes("/portal") && authenticated) {
        const dashboardPath = pathnameHasLocale
            ? `${pathname}/dashboard`
            : `/${locale}${pathname}/dashboard`;
        if (pathname === `/${locale}/portal`) {
            request.nextUrl.pathname = dashboardPath;
            return Response.redirect(request.nextUrl);
        }
        if (pathnameHasLocale) return;
        request.nextUrl.pathname = `/${locale}${pathname}`;
        return Response.redirect(request.nextUrl);
    }

    if (pathname.includes("/portal") && !authenticated) {
        request.nextUrl.pathname = `/${locale}/`;
        return Response.redirect(request.nextUrl);
    }

    if (!pathname.includes("/portal")) {
        if (pathnameHasLocale) return;
        request.nextUrl.pathname = `/${locale}${pathname}`;
        return Response.redirect(request.nextUrl);
    }
}

export const config = {
    matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

AmplifyServerUtils.tsx

import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import config from "@/amplifyconfiguration.json";

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

Thanks in advance.

israx commented 8 months ago

hello @muratcali . It seems that fetchAuthSession is being called in the middleware before the user is authenticated. Hence the UserUnAuthenticatedException: User needs to be authenticated to call this API exception. Can you see the network requests after calling the signIn API and see if there are any calls to InitiateAuth and ResponseToAuthChallenge ?

ryanwalters commented 8 months ago

I am experiencing this error as well. In my scenario (which may differ from the OP's), the issue appears to be happening only when I'm using a custom hostname during development; the issue does not occur when using localhost.

Here is a test repo with repro steps in the readme: https://github.com/ryanwalters/amplify-6

If I were to hazard a guess, Amplify is successfully writing the cookies but to the wrong domain. So when it tries to read those cookies to call getCurrentUser we're "not authenticated".

Maybe there should be an option to pass an instance of CookieStorage to libraryOptions?

Amplify.configure(config, {
  ssr: true,
  cookieStorage: new CookieStorage({
    domain: 'mydomain.com',
    ...
  }),
});
HuiSF commented 7 months ago

HiĀ @ryanwalters Currently,Ā adapter-nextjs doesn't provide an interface for configuring theĀ SetCookieĀ options from the server side. Therefore, if the cookies have been updated from the server side, theĀ domain field will be the default value. We are looking into expanding the interface with supporting other relevant features. In the meantime, if you are looking for fully customizable SetCookie options on the server side, you may also consider using the low-level adapter exported from the aws-amplify/adapter-core. SeeĀ this documentĀ for details.

HuiSF commented 7 months ago

Hi @muratcali thanks for providing more details. Could you do the following for verifying a few things:

  1. set a break pointer in the debug mode, at the line of console.log(isSignedIn, nextStep); in your handleSignIn function
  2. when the debugger reaches to this point, can you check the cookie store of your web app, to verify whether the auth tokens have been written into the cookie store

Thanks.

cwomack commented 7 months ago

@muratcali, just wanted to check in and see if you saw @HuiSF's comment and recommendations above. Can you also see if disabling the middleware changes anything to see if we can isolate it to that even further? Thanks

cwomack commented 6 months 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!

didemkkaslan commented 6 months ago

I have the same issue, auth tokens are not stored for the ms teams tab. So I get user needs to be authenticated to call this api error.

HuiSF commented 6 months ago

auth tokens are not stored for the ms teams tab

By "ms teams tab", is this tab under a different domain (other than the domain you have your end users signed in)? @didemkkaslan