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

Intermittent 403 signature error when launching React Native app #4584

Closed willdady closed 4 years ago

willdady commented 4 years ago

Describe the bug Intermittently when launching React Native app ALL requests to API Gateway fail with a 403 error. Checking the API Gateway logs in CloudWatch shows they fail with:

The request signature we calculated does not match the signature you provided.

App authenticates using Auth0. I am not using any HoCs from aws-amplify-react-native.

It's worth mentioning the symptoms are very similar to #3270 in that the issue only seems to manifest in production builds making it very hard to debug.

To Reproduce App has the following setup:

  1. Sign in using Auth0 flow + Auth.federatedSignIn(). Confirm calls to API Gateway endpoint using IAM authorizer work as expected.
  2. Close app (swipe away to kill the process) and re-open. Again, confirm calls to API Gateway endpoint using IAM authorizer work as expected.
  3. Repeatedly close the app and re-open it. Occasionally the app will launch and fail to load data with ALL requests to API Gateway failing with 403. Most of the time the app launches fine but around 1 out of 10 launches the issue occurs. The only way to get data to load is to close the app and re-open.

It feels like there is some kind of race condition happening during Amplify's initialisation. Like maybe various async setup functions are resolving out-of-order?

Expected behavior App should work consistently given the same actions.

Environment ``` System: OS: macOS 10.14.6 CPU: (8) x64 Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz Memory: 2.18 GB / 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 10.17.0 - ~/.nvm/versions/node/v10.17.0/bin/node Yarn: 1.12.3 - /usr/local/bin/yarn npm: 6.11.3 - ~/.nvm/versions/node/v10.17.0/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman Browsers: Chrome: 79.0.3945.79 Firefox: 66.0.5 Safari: 13.0.4 npmPackages: @babel/core: ^7.4.4 => 7.4.5 @babel/runtime: ^7.4.4 => 7.4.5 @ptomasroos/react-native-multi-slider: ^0.0.14 => 0.0.14 @react-native-community/async-storage: 1.5.1 => 1.5.1 @storybook/react-native: ^4.1.11 => 4.1.11 @svgr/cli: ^4.1.0 => 4.1.0 assert: 1.4.1 => 1.4.1 aws-amplify: 2.2.0 => 2.2.0 aws-amplify-react-native: 2.2.3 => 2.2.3 babel-eslint: ^10.0.1 => 10.0.1 babel-jest: ^24.8.0 => 24.8.0 babel-plugin-inline-import: 2.0.6 => 2.0.6 babel-plugin-transform-remove-console: ^6.9.4 => 6.9.4 babel-plugin-transform-replace-object-assign: 1.0.0 => 1.0.0 babel-polyfill: ^6.26.0 => 6.26.0 babel-preset-react-native: 4.0.0 => 4.0.0 crypto-js: 3.1.9-1 => 3.1.9-1 dotenv: 4.0.0 => 4.0.0 enzyme: 3.0.0 => 3.0.0 enzyme-adapter-react-16: 1.0.0 => 1.0.0 eslint: 4.6.1 => 4.6.1 eslint-config-airbnb: 15.1.0 => 15.1.0 eslint-config-prettier: 6.5.0 => 6.5.0 eslint-plugin-import: 2.7.0 => 2.7.0 eslint-plugin-jsx-a11y: 5.1.1 => 5.1.1 eslint-plugin-prettier: 2.2.0 => 2.2.0 eslint-plugin-react: 7.3.0 => 7.3.0 expo-contacts: ^4.0.0 => 4.0.0 hoist-non-react-statics: ^3.0.1 => 3.0.1 https: ^1.0.0 => 1.0.0 imagemin-cli: 3.0.0 => 3.0.0 jest: ^24.8.0 => 24.8.0 jest-expo: 19.0.0 => 19.0.0 jetifier: ^1.6.4 => 1.6.4 jwt-decode: ^2.2.0 => 2.2.0 lodash: 4.17.4 => 4.17.4 metro-react-native-babel-preset: ^0.54.0 => 0.54.1 moment: ^2.22.0 => 2.22.0 path-parser: 2.0.2 => 2.0.2 prettier: 1.19.1 => 1.19.1 prop-types: 15.6.0 => 15.6.0 query-string: ^6.1.0 => 6.1.0 react: 16.8.3 => 16.8.3 react-dom: 16.0.0-alpha.12 => 16.0.0-alpha.12 react-native: 0.59.8 => 0.59.8 react-native-actionsheet: 2.2.2 => 2.2.2 react-native-animatable: 1.2.4 => 1.2.4 react-native-auth0: ^1.3.0 => 1.3.0 react-native-collapsible: ^1.5.0 => 1.5.0 react-native-communications: 2.2.1 => 2.2.1 react-native-config: ^0.11.5 => 0.11.5 react-native-device-info: 5.3.0 => 5.3.0 react-native-fast-image: 7.0.2 => 7.0.2 react-native-fbsdk: ^0.8.0 => 0.8.0 react-native-firebase: ^5.2.1 => 5.2.1 react-native-gesture-handler: 1.3.0 => 1.3.0 react-native-gifted-chat: 0.10.2 => 0.10.2 react-native-image-crop-picker: ^0.24.1 => 0.24.1 react-native-intercom: 13.0.1 => 13.0.1 react-native-keyboard-aware-scroll-view: ^0.8.0 => 0.8.0 react-native-keychain: 2.0.0-rc => 2.0.0-rc react-native-kochava-tracker: ^1.1.0 => 1.1.0 react-native-maps: ^0.23.0 => 0.23.0 react-native-permissions: 1.1.1 => 1.1.1 react-native-rate: ^1.1.6 => 1.1.6 react-native-schemes-manager: ^1.0.4 => 1.0.4 react-native-screens: ^1.0.0-alpha.16 => 1.0.0-alpha.16 react-native-sentry: 0.43.2 => 0.43.2 react-native-share: 1.2.1 => 1.2.1 react-native-svg: 9.5.1 => 9.5.1 react-native-swiper: ^1.5.14 => 1.5.14 react-native-unimodules: ^0.3.1 => 0.3.1 react-native-video: 5.0.2 => 5.0.2 react-native-view-more-text: ^2.1.0 => 2.1.0 react-native-webview: 7.0.1 => 7.0.1 react-navigation: ^3.0.0 => 3.9.1 react-navigation-backhandler: ^1.1.1 => 1.1.1 react-navigation-tabs: ^0.8.4 => 0.8.4 react-redux: ^7.1.0 => 7.1.0 react-test-renderer: 16.8.3 => 16.8.3 reactotron-react-native: ^2.1.4 => 2.1.4 reactotron-redux: ^2.1.3 => 2.1.3 redux: ^4.0.4 => 4.0.4 redux-logger: 3.0.6 => 3.0.6 redux-mock-store: 1.3.0 => 1.3.0 redux-persist: 5.10.0 => 5.10.0 redux-thunk: 2.2.0 => 2.2.0 semver: ^5.6.0 => 5.6.0 tinycolor2: ^1.4.1 => 1.4.1 url: 0.11.0 => 0.11.0 uuid: ^3.3.2 => 3.3.2 validate.js: ^0.12.0 => 0.12.0 npmGlobalPackages: fsevents: 2.1.2 npm: 6.11.3 react-native: 0.61.4 ```

Smartphone (please complete the following information):

Happening on several other Android devices also. I'm not 100% sure if this issue occurs on iOS.

elorzafe commented 4 years ago

@willdady can you paste a code snippet of your App to see how you are doing sign-in. Also how are you refreshing credentials?

willdady commented 4 years ago

@elorzafe as requested.

Amplify config. Note Config is react-native-config.

Amplify.configure({
  Auth: {
    identityPoolId: Config.AWS_IDENTITY_POOL_ID,
    userPoolId: Config.AWS_USER_POOL_ID,
    userPoolWebClientId: Config.AWS_USER_POOL_CLIENT_ID,
    region: Config.AWS_REGION,
    mandatorySignIn: true,
    refreshHandlers: {
      [Config.AUTH0_DOMAIN]: AuthService.refreshHandler
    }
  },
  API: {
    endpoints: [
      {
        name: 'GoodWorkAPI',
        endpoint: Config.API_DOMAIN,
        region: Config.AWS_REGION,
        custom_header: async () => ({
          'User-Agent': USER_AGENT,
          'GW-Client-App': Config.API_APPLICATION
        })
      }
    ]
  }
});

Below is AuthService a convenience module which is an abstraction over both Amplify's and Auth0's methods.

import { AsyncStorage } from 'react-native';
import { Auth } from 'aws-amplify';
import moment from 'moment';
import Auth0 from 'react-native-auth0';
import Config from 'react-native-config';
import jwtDecode from 'jwt-decode';
import get from 'lodash/get';

import Logger from './logger';
import { store } from '../store';
import { logout } from '../actions/auth';

const AUTH0 = new Auth0({
  domain: Config.AUTH0_DOMAIN,
  clientId: Config.AUTH0_CLIENT_ID
});

const federatedSignIn = async ({ idToken, expiresAt, name }) =>
  Auth.federatedSignIn(
    Config.AUTH0_DOMAIN,
    {
      token: idToken,
      expires_at: expiresAt
    },
    {
      name
    }
  );

const auth0Authorize = async () => {
  // Launches the Auth0 Authentication View
  const credentials = await AUTH0.webAuth.authorize({
    scope: 'openid offline_access profile',
    audience: Config.AUTH0_AUDIENCE_URL
  });
  const profile = await AUTH0.auth.userInfo({ token: credentials.accessToken });
  return {
    phone: profile.name,
    name: profile.name,
    sub: profile.sub,
    accessToken: credentials.accessToken,
    idToken: credentials.idToken,
    refreshToken: credentials.refreshToken,
    expiresIn: credentials.expiresIn
  };
};

const setRefreshToken = async token => AsyncStorage.setItem('GW_RefreshToken', token);

const getRefreshToken = async () => AsyncStorage.getItem('GW_RefreshToken');

const signOut = () => Auth.signOut();

// eslint-disable-next-line no-shadow
const refreshToken = async refreshToken => AUTH0.auth.refreshToken({ refreshToken });

const getCurrentCredentials = async () => Auth.currentCredentials();

const getCurrentAuthenticatedUser = async () => Auth.currentAuthenticatedUser();

const tokenIsExpired = token => {
  const decodedToken = jwtDecode(token);
  const tokenExpiry = moment.unix(decodedToken.exp);
  return moment().isAfter(tokenExpiry);
};

const getTokenExpiryAsMilliseconds = token => jwtDecode(token).exp * 1000;

/**
 * Attempts to refresh authentication token. Used by Amplify.Auth.
 */
const refreshHandler = async () => {
  Logger.debug('Token refreshing!');
  let idToken;
  // eslint-disable-next-line no-underscore-dangle
  let _refreshToken;
  try {
    _refreshToken = await getRefreshToken();
  } catch (err) {
    Logger.error(
      `Error reading refresh token from storage!: ${get(err, 'message', 'No message in error')}`
    );
    Logger.captureException(err);
    throw err;
  }
  if (!_refreshToken) {
    Logger.error('refreshToken is falsy');
    throw new Error('refreshToken is falsy');
  }
  try {
    const resp = await refreshToken(_refreshToken);
    idToken = resp.idToken;
  } catch (err) {
    Logger.error(`Token refreshing failed!: ${get(err, 'message', 'No message in error')}`);
    Logger.captureException(err);
    store.dispatch(logout());
    throw err;
  }
  Logger.debug('Token refreshed successfully!');
  return Promise.resolve({
    token: idToken,
    expires_at: getTokenExpiryAsMilliseconds(idToken)
  });
};

export default {
  federatedSignIn,
  auth0Authorize,
  setRefreshToken,
  getRefreshToken,
  signOut,
  refreshToken,
  getCurrentCredentials,
  getCurrentAuthenticatedUser,
  tokenIsExpired,
  getTokenExpiryAsMilliseconds,
  refreshHandler
};

Below is the redux action auth0Authentication (a thunk) used to trigger sign in. Note I've excluded the code which runs after federatedSignIn which basically triggers navigation to the app's 'Home' screen.

const auth0Webview = async () => {
  StatusBar.setBarStyle('dark-content');
  return AuthService.auth0Authorize();
};

export function auth0Authentication() {
  return async dispatch => {
    dispatch(setAuthError(''));
    dispatch(setUserAuthenticating(true));
    try {
      const { phone, name, sub, idToken, refreshToken } = await auth0Webview(dispatch);
      await AuthService.setRefreshToken(refreshToken);
      const FCMToken = await getFCMToken();
      const expiresAt = AuthService.getTokenExpiryAsMilliseconds(idToken);
      await AuthService.federatedSignIn({ idToken, expiresAt, name });
      // -- Snip --
    } catch (err) {
      dispatch(logout());
      // The auth0Webview() call will raise an exception if the user closes the webview.
      // We don't log this as it's a non-error.
      if (get(err, 'error') === 'a0.session.user_cancelled') return;
      let errorMessage = get(err, 'message', DEFAULT_AUTH_ERROR_MESSAGE);
      if (
        errorMessage.includes('Signature expired') ||
        errorMessage.includes('Signature not yet current')
      ) {
        errorMessage = `${errorMessage} - Please check your device's time is set correctly`;
      }
      dispatch(setAuthError(errorMessage));
      Logger.captureException(err);
    }
  };
}

To clarify signing in works fine. It's when opening the app after having previously signed in when it randomly fails.

An important thing to note is that as soon as the app's 'home' screen mounts the app immediately does 4 requests to API Gateway. Each of the 4 requests hit different endpoints, each secured with IAM authorizer. When this issue happens it seems all requests the REST client sends are signed incorrectly. It feel like it's a race condition inside Amplify and once it breaks it's broken until the app is restarted.

Also when checking logs in Sentry it's suspicious that 2 calls are sent to https://cognito-identity.ap-southeast-2.amazonaws.com/ at the same time.

See screenshot: https://imgur.com/a/lOvEpuN

willdady commented 4 years ago

I was able to solve this by upgrading React Native from 0.59.8 to 0.59.10. This was an extremely frustrating issue. No idea what changed between those versions. Closing issue.

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.