Search code examples
javascriptauthenticationreact-adminmulti-factor-authentication

Any suggestions on how to implement challenge (2 factor authentication, etc...) in authentication


I am trying to incorporate 2FA in the react admin login flow.

The issue is that the standard way to validate a login is to use useLogin.

const login = useLogin();

try {
  await login({username: "joeblack", password: "mybadpassword"}, "/redirectlocation");
} catch (err) {
  // display an error notice or whatever
}

Basically, the login function from useLogin will either complete the login process and log the user in or show an error.

Second Authentication Step

For things like 2FA, new password required, etc..., there needs to be an in between step where the user isn't authenticated yet to view resources, but is not in an error state.

So for instance, perhaps login would return a challenge with the type of challenge.

Technically this can be done by returning that info in the authProvider login function and then making decisions based on that.

const loginResult = login({username: "joeblack", password: "mybadpassword"});

// loginResult returns { challenge: "2FA" }

if (challenge) {
  // redirect to challenge page
} else {
  // redirect to dashboard or wherever
}

The issue is that even if we handle it after the login function, once that login function runs, technically the user is authenticated. So they could just bypass the challenge and directly input the resource they want and they would be able to view it.

The login function of the authProvider only has 2 results, a resolved promise or rejected promise.

Before I go further to figure out how to make this work I wanted to see if anyone else has looked into this issue.


Solution

  • This is basically a workaround, but I think it's the best option for now (although I'd love to be proven wrong).

    The idea is that even though Amplify's auth comes back with a resolved promise containing the Cognito user, we check if there is a challenge property. If there is, then we reject the login promise so react-admin doesn't log the user in.

    Then we return an error message in the rejected promise that can be read by whatever is calling the login function. If it's a challenge then the login interface can handle it appropriately presenting a 2FA screen to confirm.

    Had to also include the Cognito user in the our case because we need that in order to confirm the 2FA.

    Here is the authProvider login function

    login: async ({ username, password }) => {
        try {
          const cognitoUser = await Auth.signIn(username, password);
          if (cognitoUser.hasOwnProperty("challengeName")) {
            return Promise.reject({
              message: cognitoUser.challengeName,
              cognitoUser: cognitoUser,
            });
          } else {
            return { ...createUser(cognitoUser) };
          }
        } catch (err) {
          return Promise.reject({ message: err.code });
        }
      }
    

    Hopefully that helps someone. Let me know if you have a better solution.