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.41k stars 2.11k forks source link

fetchAuthSession throws TooManyRequestsException #13565

Open didemkkaslan opened 1 month ago

didemkkaslan commented 1 month ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

REST API

Amplify Version

v6

Amplify Categories

api

Backend

CDK

Environment information

``` # Put output below this line System: OS: macOS 14.5 CPU: (8) arm64 Apple M2 Memory: 219.05 MB / 8.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 18.16.0 - /usr/local/bin/node Yarn: 1.22.22 - ~/spiky-projects/platform-v2/node_modules/.bin/yarn npm: 9.5.1 - /usr/local/bin/npm bun: 1.0.2 - ~/.bun/bin/bun Browsers: Chrome: 126.0.6478.127 Safari: 17.5 npmPackages: @amplitude/analytics-browser: ^2.3.3 => 2.4.1 @ampproject/toolbox-optimizer: undefined () @ant-design/cssinjs: 1.20.0 => 1.20.0 @ant-design/icons: ^5.2.6 => 5.3.0 (5.3.7) @ant-design/plots: ^1.2.5 => 1.2.6 @aws-amplify/adapter-nextjs: 1.2.4 => 1.2.4 @aws-amplify/adapter-nextjs/api: undefined () @aws-amplify/adapter-nextjs/data: undefined () @babel/core: undefined () @babel/runtime: 7.15.4 @edge-runtime/cookies: 4.0.2 @edge-runtime/ponyfill: 2.4.1 @edge-runtime/primitives: 4.0.2 @graphql-codegen/cli: 5.0.0 => 5.0.0 @graphql-codegen/client-preset: 4.1.0 => 4.1.0 @graphql-codegen/introspection: 4.0.0 => 4.0.0 @hapi/accept: undefined () @mantine/hooks: ^7.1.5 => 7.5.2 @microsoft/teams-js: ^2.19.0 => 2.23.0 @mswjs/interceptors: undefined () @napi-rs/triples: undefined () @next/bundle-analyzer: ^13.5.6 => 13.5.6 @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @react-pdf/renderer: ^3.1.13 => 3.3.8 @segment/ajv-human-errors: undefined () @tailwindcss/typography: ^0.5.10 => 0.5.10 @tanstack/query-codemods: 4.24.3 @tanstack/react-query: ^5.0.5 => 5.20.5 @tanstack/react-query-devtools: ^5.8.9 => 5.20.5 @testing-library/jest-dom: ^6.1.4 => 6.4.2 @testing-library/react: ^14.0.0 => 14.2.1 @testing-library/user-event: ^14.5.1 => 14.5.2 @types/jest: ^29.5.6 => 29.5.12 @types/lodash: ^4.14.200 => 4.14.202 @types/mixpanel-browser: ^2.47.4 => 2.49.0 @types/node: ^20.8.8 => 20.11.17 (16.18.87) @types/react: ^18.2.31 => 18.2.55 @types/react-dom: ^18.2.14 => 18.2.19 @types/react-google-recaptcha: ^2.1.7 => 2.1.9 @types/react-highlight-words: ^0.16.6 => 0.16.7 @types/uuid: ^9.0.7 => 9.0.8 @typescript-eslint/eslint-plugin: ^6.9.0 => 6.21.0 @typescript-eslint/parser: ^6.9.0 => 6.21.0 @vercel/nft: undefined () @vercel/og: undefined () acorn: undefined () amphtml-validator: undefined () anser: undefined () antd: 5.17.0 => 5.17.0 apexcharts: ^3.44.0 => 3.45.2 arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () autoprefixer: ^10.4.16 => 10.4.17 aws-amplify: 6.3.6 => 6.3.6 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 () aws-rum-web: ^1.15.0 => 1.17.0 axios: ^1.5.1 => 1.6.7 babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 clsx: ^2.0.0 => 2.1.0 comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () content-disposition: undefined () content-type: undefined () cookie: undefined () cookies-next: ^4.1.1 => 4.1.1 cross-spawn: undefined () crypto-browserify: undefined () css.escape: undefined () data-uri-to-buffer: undefined () dayjs: ^1.11.10 => 1.11.10 debug: undefined () devalue: undefined () docx: ^8.5.0 => 8.5.0 domain-browser: undefined () edge-runtime: undefined () eslint: ^8.52.0 => 8.56.0 eslint-config-airbnb: ^19.0.4 => 19.0.4 eslint-config-airbnb-typescript: ^17.1.0 => 17.1.0 eslint-config-next: ^13.5.6 => 13.5.6 eslint-config-prettier: ^9.0.0 => 9.1.0 eslint-plugin-i18next: ^6.0.3 => 6.0.3 eslint-plugin-import: ^2.29.0 => 2.29.1 eslint-plugin-jest: ^27.4.3 => 27.8.0 eslint-plugin-jest-dom: ^5.1.0 => 5.1.0 eslint-plugin-jsx-a11y: ^6.7.1 => 6.8.0 eslint-plugin-react: ^7.33.2 => 7.33.2 eslint-plugin-testing-library: ^6.1.2 => 6.2.0 events: undefined () find-cache-dir: undefined () find-up: undefined () framer-motion: ^10.16.4 => 10.18.0 fresh: undefined () get-orientation: undefined () glob: undefined () graphql: ^16.8.1 => 16.8.1 (15.8.0) graphql-request: ^6.1.0 => 6.1.0 gzip-size: undefined () http-proxy: undefined () http-proxy-agent: undefined () https-browserify: undefined () https-proxy-agent: undefined () husky: ^8.0.3 => 8.0.3 i18next: ^23.7.15 => 23.8.2 i18next-chained-backend: ^4.5.0 => 4.6.2 i18next-http-backend: ^2.2.2 => 2.4.3 i18next-localstorage-backend: ^4.2.0 => 4.2.0 icss-utils: undefined () ignore-loader: undefined () image-size: undefined () is-animated: undefined () is-docker: undefined () is-wsl: undefined () jest: ^29.7.0 => 29.7.0 jest-environment-jsdom: ^29.7.0 => 29.7.0 jest-worker: undefined () json5: undefined () jsonwebtoken: undefined () jwt-decode: ^3.1.2 => 3.1.2 loader-runner: undefined () loader-utils: undefined () lodash: ^4.17.21 => 4.17.21 lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () mixpanel-browser: ^2.47.0 => 2.49.0 nanoid: undefined () native-url: undefined () neo-async: undefined () next: ^13.5.6 => 13.5.6 next-i18next: ^15.1.2 => 15.2.0 next-i18next-create-client: undefined () next-seo: ^6.1.0 => 6.4.0 node-fetch: undefined () node-html-parser: undefined () nookies: ^2.5.2 => 2.5.2 ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss: ^8.4.31 => 8.4.35 (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 () prettier: ^3.0.3 => 3.2.5 prettier-plugin-tailwindcss: ^0.5.6 => 0.5.11 process: undefined () punycode: undefined () querystring-es3: undefined () raw-body: undefined () react: 18.2.0 => 18.2.0 react-apexcharts: ^1.4.1 => 1.4.1 react-builtin: undefined () react-dom: 18.2.0 => 18.2.0 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-error-boundary: ^4.0.13 => 4.0.13 react-experimental-builtin: undefined () react-google-recaptcha: ^3.1.0 => 3.1.0 react-highlight-words: ^0.20.0 => 0.20.0 react-i18next: ^14.0.0 => 14.0.5 react-icons: ^4.11.0 => 4.12.0 react-infinite-scroll-component: ^6.1.0 => 6.1.0 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 () sharp: ^0.32.6 => 0.32.6 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: ^1.14.0 => 1.14.0 tailwindcss: ^3.3.3 => 3.4.1 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () typescript: ^5.2.2 => 5.3.3 ua-parser-js: undefined () undici: undefined () unistore: undefined () usehooks-ts: ^2.9.1 => 2.14.0 util: undefined () uuid: ^9.0.1 => 9.0.1 (8.3.2) vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: ^3.22.4 => 3.22.4 () zustand: ^4.5.2 => 4.5.2 npmGlobalPackages: corepack: 0.17.0 eas-cli: 8.0.0 expo-cli: 6.3.10 npm: 9.5.1 turbo: 1.13.3 ```

Describe the bug

Hello, I'm using fetchAuthSession to get idtoken and use it as Authorization header. And also I use it to get user scopes I started to get TooManyRequestsException is there anything I should shouldn't do? Not sure how to approach this problem. Thanks in advance

Screenshot 2024-07-05 at 07 56 24
import LoadingIcon from '@/shared/components/LoadingIcon';
import { useQuery } from '@tanstack/react-query';
import { fetchAuthSession } from 'aws-amplify/auth';
import { createContext, useCallback, useContext, useMemo } from 'react';

enum SCOPE {
  HOMEPAGE_TEAM_TAB = 'HOMEPAGE_TEAM_TAB',
  MEETINGS_MANUAL_UPLOAD = 'MEETINGS_MANUAL_UPLOAD',
  //....
}

type Feature =
  | 'isHomepageTeamTabEnabled'
  | 'isMeetingsManualUploadEnabled'
 //...

const FEATURE_TO_SCOPE: Record<Feature, SCOPE> = {
  isHomepageTeamTabEnabled: SCOPE.HOMEPAGE_TEAM_TAB,
  isMeetingsManualUploadEnabled: SCOPE.MEETINGS_MANUAL_UPLOAD,
  //...
};

interface ScopeState {
  scopes: string[] | undefined;
}

const ScopesContext = createContext<ScopeState>({
  scopes: [],
});

export default function ScopesProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['scopes'],
    queryFn: async () => {
      try {
        const session = await fetchAuthSession(); // will return the credentials
        return session?.tokens?.idToken?.payload?.scope?.split(' ') ?? [];
      } catch (e) {
        console.error('Error fetching scopes:', e);
        return [];
      }
    },
  });
  console.log('Scopes are:', data);
  const memoizedValue = useMemo(() => ({ scopes: data }), [data]);

  if (isLoading) {
    return (
      <div className="flex justify-center items-center h-screen">
        <LoadingIcon loading />
      </div>
    );
  }

  if (isError) {
    return <div>Error fetching scopes</div>;
  }

  return (
    <ScopesContext.Provider value={memoizedValue}>
      {children}
    </ScopesContext.Provider>
  );
}

export const useScopes = () => {
  const context = useContext(ScopesContext);

  if (!context) {
    throw new Error('useScopes must be used within a ScopesProvider');
  }

  const isFeatureEnabled = useCallback(
    (scope: SCOPE) => context?.scopes?.includes(scope),
    [context?.scopes],
  );

  const features = useMemo(
    () =>
      Object.keys(FEATURE_TO_SCOPE).reduce(
        (acc, key) => ({
          ...acc,
          [key]: isFeatureEnabled(FEATURE_TO_SCOPE[key as Feature]),
        }),
        {} as Record<Feature, boolean>,
      ),
    [isFeatureEnabled],
  );

  return {
    ...features,
  };
};
import amplifyconfig from '../../amplifyconfiguration.json';
import { Amplify } from 'aws-amplify';
import { fetchAuthSession } from 'aws-amplify/auth';
import { parseAmplifyConfig } from 'aws-amplify/utils';
import { createKeyValueStorageFromCookieStorageAdapter } from 'aws-amplify/adapter-core';
import { deleteCookie, getCookie, setCookie, getCookies } from 'cookies-next';
import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito';

const amplifyConfig = parseAmplifyConfig(amplifyconfig);

const cookieOptions =
  process.env.NEXT_PUBLIC_ENV === 'msteams'
    ? {
        domain: 'tab.app.spiky.ai' as string,
        sameSite: 'none' as 'lax' | 'strict' | 'none',
        secure: true,
      }
    : {};

const keyValueStorage = createKeyValueStorageFromCookieStorageAdapter({
  get(name) {
    const value = getCookie(name, cookieOptions);
    return { name, value };
  },
  getAll() {
    const cookies = getCookies(cookieOptions);
    return Object.keys(cookies).map((name) => ({ name, value: cookies[name] }));
  },
  set(name, value) {
    setCookie(name, value, cookieOptions);
  },
  delete(name) {
    deleteCookie(name, cookieOptions);
  },
});

export const getAuthToken = async () => {
  const session = await fetchAuthSession({});
  return session.tokens?.idToken?.toString() as string;
};

export function configureAmplify() {
  Amplify.configure(
    {
      ...amplifyConfig,
      Auth: {
        ...amplifyConfig.Auth,
        Cognito: {
          ...amplifyConfig.Auth?.Cognito,
          identityPoolId:
            process.env.NEXT_PUBLIC_PLATFORM_COGNITO_IDENTITY_POOL_ID!,
          userPoolId: process.env.NEXT_PUBLIC_PLATFORM_COGNITO_USER_POOL_ID!,
          userPoolClientId:
            process.env.NEXT_PUBLIC_PLATFORM_COGNITO_USER_POOL_WEB_CLIENT_ID!,
        },
      },
      API: {
        ...amplifyConfig.API,
        REST: {
          ...amplifyConfig.API?.REST,
          PlatformCorePublicRestApi: {
            endpoint: `${process.env.NEXT_PUBLIC_PLATFORM_CORE_REST_API_ENDPOINT}/public`,
          },
          PlatformCoreRestApi: {
            endpoint: `${process.env.NEXT_PUBLIC_PLATFORM_CORE_REST_API_ENDPOINT}/platform`,
          },
          PlatformCoreCalendarRestApi: {
            endpoint: `${process.env.NEXT_PUBLIC_PLATFORM_CORE_REST_API_ENDPOINT}/calendar-v2`,
          },
          PlatformCoreTeamsRestApi: {
            endpoint: `${process.env.NEXT_PUBLIC_PLATFORM_CORE_REST_API_TEAMS_ENDPOINT}`,
          },
          PlatformIntegrationRestApi: {
            endpoint: `${process.env.NEXT_PUBLIC_PLATFORM_INTEGRATION_REST_API_ENDPOINT}`,
          },
          PlatformRestApi: {
            endpoint: `${process.env.NEXT_PUBLIC_PLATFORM_REST_API_ENDPOINT}`,
          },
        },
      },
    },
    {
      ssr: true,
      API: {
        REST: {
          headers: async () => ({
            Authorization: `Bearer ${await getAuthToken()}`,
          }),
        },
      },
    },
  );

  cognitoUserPoolsTokenProvider.setKeyValueStorage(keyValueStorage);
}

Expected behavior

No rate limit errors

Reproduction steps

Use fetchAuthSession multiple places in the app

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

HuiSF commented 1 month ago

Hi @didemkkaslan Thanks for opening this issue.

By looking at your sample code, you are implementing a ScopesContext for your Client Components (as all the context APIs, such as createContext, useContext, and Provider, can only be used in Client Components in a Next.js app) along with the usage of the Amplify client-side fetchAuthSession() API. If this is what you want to achieve, you only need to invoke Amplify.configure(config, { ssr: true }) (note the second parameter) on the client side.

In addition, the createKeyValueStorageFromCookieStorageAdapter utility function is meant for use along with the generic Amplify SSR adapter exported from aws-amplify/adapter-core, you should not need to use it in a Next.js app while @aws-amplify/adapter-nextjs should be used.

Can you try to remove the useage of createKeyValueStorageFromCookieStorageAdapter cognitoUserPoolsTokenProvider.setKeyValueStorage(keyValueStorage);?

didemkkaslan commented 1 month ago

Hello @HuiSF sorry for the late reply. I had to use createKeyValueStorageFromCookieStorageAdapter because we have a microsoft tab app implementation and there were issues related to authentication before implementing createKeyValueStorageFromCookieStorageAdapter

I had to include below code to make it work

const cookieOptions =
  process.env.NEXT_PUBLIC_ENV === 'msteams'
    ? {
        domain: 'tab.app.spiky.ai' as string,
        sameSite: 'none' as 'lax' | 'strict' | 'none',
        secure: true,
      }
    : {};
didemkkaslan commented 1 month ago

I'm a bit confused about if it is required to refetch scopes for token refresh events or should I only refetch the scopes when signIn or signInWithRedirect occurs

didemkkaslan commented 1 month ago

I've realised I haven't share the code I get the TooManyRequestsException error. So the problem is tokenRefresh event is fired many times so fetchAuthSession is called many times and we have the error

/* eslint-disable consistent-return */
/* eslint-disable default-case */
import { PeopleData } from '@/people/types/PeopleData';
import getPersonName from '@/people/utils/getPersonName';
import { getCurrentUser } from '@/settings/service/userService';
import { ONBOARDING_INTRODUCED_AT } from '@/shared/lib/getOnboardingStatus';
import { bootIntercom, shutdownIntercom } from '@/shared/lib/intercom';
import generateUserHash from '@/shared/services/generateUserHash';
import { useQueryClient } from '@tanstack/react-query';
import { fetchAuthSession } from 'aws-amplify/auth';
import { Hub } from 'aws-amplify/utils';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';

interface IdentificationProps {
  children: React.ReactNode;
}

const sendUserDetailsToIntercom = async (user: PeopleData) => {
  shutdownIntercom();
  const userHash = await generateUserHash(user?.id);
  const username = getPersonName(user);
  bootIntercom({
    app_id: process.env.NEXT_PUBLIC_INTERCOM_APP_ID,
    name: username,
    email: user?.email,
    user_id: user?.id,
    user_hash: userHash,
    'Job Title': user?.title,
    Company: user?.company?.companyName,
    'Referral Source': user?.referralSource,
    'Role Level': user?.roleLevel,
    Department: user?.department,
    'Company Size': user?.company?.companySize,
    company: {
      id: user?.company?.id,
      name: user?.company?.companyName,
    },
  });
};

export default function Identification({ children }: IdentificationProps) {
  const queryClient = useQueryClient();
  const router = useRouter();

  const hubListenerCancelToken = Hub.listen('auth', async ({ payload }) => {
    if (payload.event !== 'signedOut') { // I get TOO MANY REQUESTS error here
      const session = await fetchAuthSession();

      queryClient.setQueryData(
        ['scopes'],
        session?.tokens?.idToken?.payload?.scope?.split(' ') ?? [],
      );
    }

    switch (payload.event) {
      case 'signInWithRedirect': {
        console.log('signInWithRedirect');
        break;
      }
      case 'signedIn': {
        console.log('signedIn');
        const user = await getCurrentUser();
        sendUserDetailsToIntercom(user);

        // Check whether the user has completed the onboarding
        const onboardingCompleted = user?.onboardingCompleted === true;
        const isAfterOnboardingIntroducedAt = dayjs(user?.createdAt).isAfter(
          ONBOARDING_INTRODUCED_AT,
        );
        const isRedirectToOnboarding =
          !onboardingCompleted && isAfterOnboardingIntroducedAt;

        if (isRedirectToOnboarding) {
          router.push('/onboarding/personal-info');
        } else {
          router.push('/meetings');
        }
        break;
      }

      case 'signedOut': {
        // Here we need to shutdown Intercom and boot it again to clear the user data when the user signs out
        shutdownIntercom();
        bootIntercom();

        break;
      }
    }
  });

  useEffect(() => {
    if (typeof window === 'undefined' || !window.Intercom) return;

    return () => {
      hubListenerCancelToken();
    };
  }, []);

  return children;
}
OrmEmbaar commented 1 month ago

@didemkkaslan I had to de-deuplicate fetchAuthSession so only a single instance of it can fire at once. See #13499.