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

Users Being Logged Out While Offline #13596

Closed matt-at-allera closed 1 month ago

matt-at-allera commented 1 month ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication

Amplify Version

v5

Amplify Categories

auth

Backend

Amplify CLI

Environment information

``` # Put output below this line System: OS: macOS 13.2.1 CPU: (8) arm64 Apple M1 Memory: 119.11 MB / 8.00 GB Shell: 5.8.1 - /bin/zsh Binaries: Node: 20.7.0 - ~/.nvm/versions/node/v20.7.0/bin/node npm: 10.1.0 - ~/.nvm/versions/node/v20.7.0/bin/npm Browsers: Chrome: 126.0.6478.127 Safari: 16.3 npmPackages: @aws-amplify/core: ^5.8.2 => 5.8.12 @aws-amplify/core/internals/aws-client-utils: undefined () @aws-amplify/core/internals/aws-client-utils/composers: undefined () @aws-amplify/core/internals/aws-clients/pinpoint: undefined () @aws-amplify/ui-react: ^5.1.1 => 5.3.3 @aws-amplify/ui-react-internal: undefined () @aws-amplify/ui-react-storage: ^2.1.4 => 2.3.3 @aws-sdk/client-cognito-identity-provider: ^3.515.0 => 3.569.0 @aws-sdk/client-ses: ^3.515.0 => 3.569.0 @aws-sdk/lib-dynamodb: ^3.515.0 => 3.569.0 @aws-sdk/util-utf8-browser: ^3.259.0 => 3.259.0 (3.6.1, 3.186.0) @babel/cli: ^7.22.10 => 7.24.5 @babel/core: ^7.22.10 => 7.24.5 @babel/plugin-proposal-private-property-in-object: ^7.21.11 => 7.21.11 (7.21.0-placeholder-for-preset-env.2) @babel/preset-env: ^7.22.10 => 7.24.5 @babel/preset-typescript: ^7.24.1 => 7.24.1 @cyntler/react-doc-viewer: ^1.14.0 => 1.14.1 @cypress/angular: 0.0.0-development @cypress/mount-utils: 0.0.0-development @cypress/react: 0.0.0-development @cypress/react18: 0.0.0-development @cypress/svelte: 0.0.0-development @cypress/vue: 0.0.0-development @cypress/vue2: 0.0.0-development @emotion/react: ^11.10.6 => 11.11.4 @emotion/styled: ^11.10.6 => 11.11.5 @mui/lab: ^5.0.0-alpha.124 => 5.0.0-alpha.170 @mui/material: ^5.11.15 => 5.15.16 @newrelic/browser-agent: ^1.260.0 => 1.260.0 @testing-library/jest-dom: ^5.16.5 => 5.17.0 @testing-library/react: ^13.4.0 => 13.4.0 @testing-library/user-event: ^13.5.0 => 13.5.0 @types/jest: ^27.5.2 => 27.5.2 @types/node: ^17.0.45 => 17.0.45 (18.19.32) @types/react: ^18.2.46 => 18.3.1 @types/react-dom: ^18.2.18 => 18.3.0 @types/react-router-dom: ^5.3.3 => 5.3.3 amazon-quicksight-embedding-sdk: ^2.6.0 => 2.7.0 aws-amplify: ^5.3.12 => 5.3.18 aws-sdk: ^2.1473.0 => 2.1615.0 aws-sdk-client-mock: ^3.0.1 => 3.1.0 axios: ^1.6.7 => 1.6.8 buffer: ^6.0.3 => 6.0.3 (4.9.2, 5.7.1) chart.js: ^4.4.3 => 4.4.3 chart.js-auto: undefined () chart.js-helpers: undefined () chartjs-plugin-datalabels: ^2.2.0 => 2.2.0 cypress: ^13.6.0 => 13.8.1 eslint-plugin-cypress: ^3.3.0 => 3.3.0 graphql: ^16.8.1 => 16.8.1 (15.8.0) html2canvas: ^1.4.1 => 1.4.1 idb: ^8.0.0 => 8.0.0 (5.0.6, 7.1.1) jest: ^29.0.0 => 29.7.0 (27.5.1) jspdf: ^2.5.1 => 2.5.1 jszip: ^3.10.1 => 3.10.1 mammoth: ^1.6.0 => 1.7.2 path-browserify: ^1.0.1 => 1.0.1 prettier: ^3.2.5 => 3.2.5 process: ^0.11.10 => 0.11.10 qrcode.react: ^3.1.0 => 3.1.0 react: ^18.2.0 => 18.3.1 (18.2.0) react-app-rewired: ^2.2.1 => 2.2.1 react-beautiful-dnd: ^13.1.1 => 13.1.1 react-chartjs-2: ^5.2.0 => 5.2.0 react-csv: ^2.2.2 => 2.2.2 react-dom: ^18.2.0 => 18.3.1 react-router-dom: ^6.24.0 => 6.24.0 react-scripts: ^5.0.1 => 5.0.1 react-select: ^5.8.0 => 5.8.0 react-signature-canvas: ^1.0.6 => 1.0.6 sass: ^1.71.1 => 1.77.0 source-map-loader: ^4.0.1 => 4.0.2 (3.0.2) swr: ^2.0.4 => 2.2.5 ts-jest: ^29.1.1 => 29.1.2 ts-loader: ^9.5.1 => 9.5.1 tsconfig-paths-webpack-plugin: ^4.1.0 => 4.1.0 typescript: ^4.9.5 => 4.9.5 web-vitals: ^2.1.4 => 2.1.4 (3.5.2) workbox-background-sync: ^6.6.0 => 6.6.0 (6.6.1) workbox-broadcast-update: ^6.6.0 => 6.6.0 workbox-cacheable-response: ^6.6.0 => 6.6.0 workbox-core: ^6.6.0 => 6.6.0 (6.6.1) workbox-expiration: ^6.6.0 => 6.6.0 workbox-google-analytics: ^6.6.1 => 6.6.1 (6.6.0) workbox-navigation-preload: ^6.6.0 => 6.6.0 workbox-precaching: ^6.6.0 => 6.6.0 workbox-range-requests: ^6.6.0 => 6.6.0 workbox-routing: ^6.6.0 => 6.6.0 (6.6.1) workbox-strategies: ^6.6.0 => 6.6.0 (6.6.1) workbox-streams: ^6.6.0 => 6.6.0 xlsx: ^0.18.5 => 0.18.5 npmGlobalPackages: @aws-amplify/cli: 12.11.1 @newrelic/publish-sourcemap: 5.1.0 aws-cdk: 2.127.0 corepack: 0.19.0 newrelic: 11.19.0 npm: 10.1.0 serve: 14.2.3 snyk: 1.1241.0 typescript: 5.2.2 ```

Describe the bug

We host our application as a PWA to enable offline usage. Users are complaining of being signed out at random times which is requiring them to go back to a WiFi enabled area before signing back in. For some of our customers, this is not acceptable as it can be a very long time before they are able to return to the ares with signal. We do NOT use DataStore, as we ran into a myriad of unrelated problems while using it before; we have our own in-house solution using IndexedDB.

Here a couple of other tickets that never reached resolution (for reference): https://github.com/aws-amplify/amplify-js/issues/10575 https://github.com/aws-amplify/amplify-flutter/issues/2806

Notably, we are using the withAuthenticator HOC:

export default withAuthenticator(App, {
  hideSignUp: true,
  components: components,
});

I have also increased the token validation period for both access and id tokens to 24 hours. It's worth noting that changing the values here did not seem to make the issue any better. I think regardless of these settings, the user should be able to remain using the application even with an invalid token:

  // Set token validity to 24 hours (default is 1)
  resources.userPoolClient.accessTokenValidity = 24;
  resources.userPoolClient.idTokenValidity = 24;

I did manage to get a screenshot captured of the debugging logs when a user is forcibly logged out. I've converted the screenshot into readable logs:

Service Worker: Online status is now offline
Offline - skipping token refresh
> POST https://cognito-idp.us-east-1.amazonaws.com/ 503 (Service Unavailable)
> POST https://cognito-idp.us-east-1.amazonaws.com/ 503 (Service Unavailable)

User signed out, Resetting is forced offline state...
In-memory cache cleared
LocalStorage keys before removal: ["NRBA_SESSION"]
LocalStorage keys after removal: ["NRBA_SESSION"]

It appears that the failed Cognito requests (which I do not know exactly what is in the contents of those requests) may have triggered a sign out event. Once the device is deemed offline (either forced offline or by actually losing network connection), we place the service-worker in a state where it rejects all requests with the 503 'Service Unavailable'. At the time of these capture logs, we also were performing a frequent session refresh to see if it alleviated this issue. It did not appear to have any positive effect.

Lastly, we do listen to 'auth' events using the Amplify Hub:

  useEffect(() => {
    const handleAuthEvents = async (data: any) => {
      const { event } = data.payload;
      if (event === 'signOut') {
        try {
          console.debug('User signed out. Resetting is forced offline state...');
          setIsForcedOffline(false);
          await clearCaches();
          clearInMemoryCache();
        } catch (err) {
          console.warn('error while processing sign out: ', err);
        } finally {
          // As an additional measure, manually remove the auth tokens from the localStorage cache
          console.debug(`LocalStorage keys before removal: ${JSON.stringify(Object.keys(localStorage))}`);
          const keysToRemove = Object.keys(localStorage).filter((key) =>
            key.startsWith('CognitoIdentityServiceProvider')
          );
          keysToRemove.forEach((key) => localStorage.removeItem(key));
          console.debug(`LocalStorage keys after removal: ${JSON.stringify(Object.keys(localStorage))}`);
        }
      }
    };

    const unsubscribe = Hub.listen('auth', handleAuthEvents);

    return () => {
      unsubscribe();
    };
  }, [clearCaches, setIsForcedOffline]);

Expected behavior

I expect that users remain logged into our Amplify app while offline. There should be no random logging out. At worst, the user should be logged out when Amplify is certain that there access and/or id tokens have expired, which does not appear to be the case.

Reproduction steps

  1. User authenticates into the Amplify app
  2. User walks to location with no wifi signal
  3. Amplify logs them out (typically within 20 minutes but we've seen it both shorter and longer)
  4. User returns to wifi area
  5. User logs back in

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

matt-at-allera commented 1 month ago

@cwomack Hi Chris, is there any triaging results to report on? This is a large blocker for specific customers of ours.

cwomack commented 1 month ago

Hey, @matt-at-allera and thanks for opening this issue. I'm working to reproduce this behavior on my side with a v5 app that runs through the reproduction steps you've outlined. Is the service worker (or others) handling anything else beyond respond with 503 errors when offline requests are made?

matt-at-allera commented 1 month ago

Thanks for the update @cwomack.

No, the service worker isn't handling anything else that is notable. I will share the relevant code in the service-worker.ts file:

// Any other custom service worker logic can go here.
let isOnline = navigator.onLine;

self.addEventListener('message', (event) => {
  if (event.data.type === 'ONLINE_STATUS') {
    isOnline = event.data.isOnline;
    console.debug(`Service Worker: Online status is now ${isOnline ? 'online' : 'offline'}`);
  }
});

self.addEventListener('fetch', (event) => {
  if (!isOnline) {
    event.respondWith(
      new Response(JSON.stringify({ error: 'Offline mode: No network connection' }), {
        status: 503,
        headers: { 'Content-Type': 'application/json' },
      })
    );
  } else {
    event.respondWith(
      (async () => {
        try {
          const response = await fetch(event.request);
          return response;
        } catch (error) {
          // Fallback response in case there is no cached response and fetch fails
          return new Response('Service is currently offline.', {
            status: 503,
            statusText: 'Service Unavailable',
            headers: new Headers({ 'Content-Type': 'text/plain' }),
          });
        }
      })()
    );
  }
});

The service worker tracks online status based upon a combination of navigator.onLine and a forceOfflineMode state variable that we relay from our app code to the service-worker via events of type 'ONLINE_STATUS' as you can see above. The logging out issue occurs for all permutations of 1) no wifi signal, 2) device wifi turned off, 3) offline mode forced, and 4) no wifi signal and offline mode forced

matt-at-allera commented 1 month ago

Hi @cwomack could you share the results of the test you performed? Is there additional information needed?

matt-at-allera commented 1 month ago

I believe I found out why this may be occurring. I noticed that if I toggle the browser to offline mode and then enable online mode, that a request to Cognito is immediately made for the REFRESH_TOKEN_AUTH auth flow

curl 'https://cognito-idp.us-east-1.amazonaws.com/' \
  -H 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'Content-Type: application/x-amz-json-1.1' \
  -H 'Cache-Control: no-store' \
  -H 'Referer: <url>/' \
  -H 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
  -H 'X-Amz-User-Agent: aws-amplify/5.0.4 @aws-amplify/ui-react/5.3.3 auth framework/1' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '{"ClientId":"<ClientId>","AuthFlow":"REFRESH_TOKEN_AUTH","AuthParameters":{"REFRESH_TOKEN":"<token>","DEVICE_KEY":null}}'

If I quickly toggle the browser back to offline, the request fails with the 503. Upon the failure, the auth seems to fire off requests that revoke all tokens in local storage:

curl 'https://cognito-idp.us-east-1.amazonaws.com/' \
  -H 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'Content-Type: application/x-amz-json-1.1' \
  -H 'Cache-Control: no-store' \
  -H 'Referer: <url>' \
  -H 'X-Amz-Target: AWSCognitoIdentityProviderService.RevokeToken' \
  -H 'X-Amz-User-Agent: aws-amplify/5.0.4 @aws-amplify/ui-react/5.3.3 auth framework/1' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '{"Token":"<Token>","ClientId":"<ClientId>"}'

I'm going to continue to experiment and see if I can hack my way around this. However, it seems like there could be some work done to make this more resilient to intermittent connections on Amplify's end. @cwomack Could you replicate this on your end and let me know what you think?

matt-at-allera commented 1 month ago

This appears to be something initiated programmatically on our end. I'm closing this issue for now.