Amplify `authState` stays `unauthenticated` after successful login - need to force-refresh useAuthenticator authState [React]

906 views Asked by At

I am using Amplify auth to handle authentication. However, after I do the Auth.signIn() and Auth.confirmSignIn() (for the user to enter the 6-digit code),

the value returned from the hook useAuthenticator for authStatus is 'unauthenticated' and for route is still 'signIn'...

I "fixed" this by just calling window.location.reload() in the promise resolve after Auth.confirmSignIn(), but it just feels wrong... and there should be a better way to notify Amplify that we have successfully signed in.

The question: How do I refresh the auth state so that the useAuthenticator picks up on my successful (custom) sign in?

Code, if needed - below

Main parts of the SignIn component:

import { Amplify, Auth } from "aws-amplify";
import { CognitoUser } from "amazon-cognito-identity-js";
// other imports, component definition and local state...

const onSubmit = () => {
    Auth.signIn({
      username: state.username,
      password: state.password,
    })
      .then((response: CognitoUser) => {
        // if needed, do something custom with the response...
        // if we have a `customChallenge`
        if (resp.challengeName) {
          // navigate to ConfirmSignIn component which renders the form to enter authentication code (from SMS or the Authenticator app)
        }
      }
};
// component render() with username and password inputs and submit button...

Main parts of the ConfirmSignIn component:

import { useAuthenticator } from "@aws-amplify/ui-react";
import { CognitoUser } from "amazon-cognito-identity-js";
// other imports, component definition and local state...

const onSubmit = useCallback(
    async (e: React.FormEvent) => {
      e.preventDefault();
      setLoading(true);
      await Auth.confirmSignIn(user, code, AmplifyChallengeName.SMS)
        .then(async csi_data => {
          // calling this doesn't work... doesn't refresh the `useAuthenticator` return values
          const cognitoUser: CognitoUser = await Auth.currentAuthenticatedUser({ bypassCache: true });
          const currentSession = await Auth.currentSession();

          // calling this doesn't work either...
          cognitoUser.refreshSession(currentSession.getRefreshToken(), (err, session) => {
            const { idToken, refreshToken, accessToken } = session;
          });

          // calling this works... but feels wrong
          // window.location.reload();
        });
      }

// component render() with code input and submit button...

and here's the Login component which has the <Authenticator>:

import { Authenticator, AuthenticatorProps, useAuthenticator } from "@aws-amplify/ui-react";
import AuthenticatorWrapper from "./AuthenticatorWrapper";
// other imports

const customizedAuthRoutes = ["signIn", "confirmResetPassword"]; //

// component definition...

  const renderCustomizedRoute = useCallback(() => {
    switch (route) {
      case "signIn":
        return <SignIn />;

      case "confirmResetPassword":
      default:
        return (
          <ConfirmResetPassword />
        );
    }
  }, [route]);

  return (
    <AuthenticatorWrapper>
      {customizedAuthRoutes.includes(route) ? (
        renderCustomizedRoute()
      ) : (
        <Authenticator
          services={services}
          className="Login"
          hideSignUp
          components={components}
          formFields={formFields}
        >
          {children}
        </Authenticator>
      )}
    </AuthenticatorWrapper>
  );

Authenticator Wrapper:

export default function AuthenticatorWrapper({ children }: PropsWithChildren) {
  const { authStatus } = useAuthenticator(context => [context.authStatus]);

  if (authStatus === "unauthenticated") {
    return (
      <div className="LogoContentCopyrightLayout">
        <Logo className="LoginLogo" />
        {children}
        <Copyright />
      </div>
    );
  }
  return <>{children}</>;
}

1

There are 1 answers

3
VonC On BEST ANSWER

In your SignIn component, you are using the Auth.signIn() method from AWS Amplify to manually sign in the user.
Then, in your ConfirmSignIn component, you are using the Auth.confirmSignIn() method to manually confirm the sign in.
So you're using the Auth methods to manually handle the sign in process.

However, in your AuthenticatorWrapper, you are using the useAuthenticator hook from AWS Amplify UI to get the authentication status.

In other words, you are using the useAuthenticator hook and the AWS Amplify Auth methods together.

The useAuthenticator hook is intended to be used with the Authenticator component to manage state and UI, while the AWS Amplify Auth methods like signIn and confirmSignIn are typically used separately to handle authentication manually.

In your situation, you are mixing these two approaches. You are manually handling the sign-in process using the Auth methods, but you are relying on the useAuthenticator hook to update your application's state.
The problem is that the useAuthenticator hook is not aware of the changes to the user's authentication status when you manually sign in the user, which is why the authStatus remains 'unauthenticated'.


A better approach would be to rely entirely on the Authenticator component and useAuthenticator hook, or to handle the authentication process manually and manage the application state yourself. The first approach is simpler and requires less code, while the second approach offers more flexibility and control.

Here is an example of how you can modify your sign-in process to use the Authenticator component and useAuthenticator hook:

SignIn Component:

import { SignIn } from "@aws-amplify/ui-react";

const MySignIn = () => {
  return <SignIn />;
};

export default MySignIn;

ConfirmSignIn Component:

import { ConfirmSignIn } from "@aws-amplify/ui-react";

const MyConfirmSignIn = () => {
  return <ConfirmSignIn />;
};

export default MyConfirmSignIn;

Login Component:

import { Authenticator } from "@aws-amplify/ui-react";
import { useState } from "react";
import MySignIn from "./MySignIn";
import MyConfirmSignIn from "./MyConfirmSignIn";

const MyLogin = () => {
  const [authState, setAuthState] = useState();
  const [user, setUser] = useState();

  return (
    <Authenticator
      onAuthStateChange={(nextAuthState, authData) => {
        setAuthState(nextAuthState);
        setUser(authData);
      }}
    >
      {authState === "signIn" && <MySignIn />}
      {authState === "confirmSignIn" && <MyConfirmSignIn />}
    </Authenticator>
  );
};

export default MyLogin;

Here, the Authenticator component automatically handles the sign-in process, and the onAuthStateChange prop is used to update the application's state when the user's authentication status changes.

Meaning: This code will render the MySignIn component when the authState is 'signIn', and the MyConfirmSignIn component when the authState is 'confirmSignIn'.
The onAuthStateChange prop of the Authenticator component will automatically update the authState and user when the authentication status changes.

This way, you will not need to use Auth.signIn() or Auth.confirmSignIn() methods manually. The Authenticator component will handle all the authentication flow for you.

If you prefer to continue handling the authentication process manually, you will need to manage the application state yourself. This could involve creating a context or a Redux store to manage the user's authentication status, and updating this state whenever the user signs in or out.