Sagar Shah
Sagar Shah

Reputation: 61

React & AWS Amplify - User does not have delivery config set to turn on SMS_MFA

I have a React web application (utilizing aws-amplify) which is connecting to AWS Cognito User Pool for authentication.

User of my application can optionally enable SMS MFA from settings.

I tried to enable SMS MFA using aws amplify npm package but I'm facing an error saying

{
  "__type": "InvalidParameterException",
  "message": "User does not have delivery config set to turn on SMS_MFA"
}

I have set MFA to "Optional" on AWS Cognito User Pools settings as seen in the screenshot below.

enter image description here

And here's my component logic

import React, { useState, useEffect } from 'react';
import { Card, Grid, Typography, Box, Switch } from '@material-ui/core';
import { Auth } from 'aws-amplify';

const Profile = () => {
  const [currentUser, setCurrentUser] = useState(null);

  // use this state to highlight MFA status
  const [isMFAEnabled, setIsMFAEnabled] = useState(false);

  const toggleMFA = async () => {
    const preferredMFA = isMFAEnabled ? 'NOMFA' : 'SMS';
    try {
        const result = await Auth.setPreferredMFA(currentUser, preferredMFA);
        setIsMFAEnabled(!isMFAEnabled);
        // Auth.enableSMS(currentUser);
    } catch (error) {
        console.log('error :: ', error);
    }
  };

  useEffect(() => {
    async function fetchProfileData() {
      const user = await Auth.currentAuthenticatedUser();
      setCurrentUser(user);
      
      // enable or disabled MFA switch
      const { preferredMFA } = user;
      setIsMFAEnabled(!(preferredMFA && preferredMFA === 'NOMFA'));
    }

    fetchProfileData();
  }, []);

  return (
    <>
      <Grid
        container
        direction="row"
        justifyContent="center"
        alignItems="center"
      >
        <Grid item xs={12} sm={12} md={10} lg={8} xl={6}>
            <Card>
                <Typography variant="h4">Security</Typography>
                <Box
                    display="flex"
                    flexDirection="row"
                    alignItems="center"
                >
                    <Typography variant="subtitle">
                        Two factor authentication
                    </Typography>
                    <Switch
                        checked={isMFAEnabled}
                        onChange={toggleMFA}
                        name="MFA"
                    />
                </Box>
            </Card>
        </Grid>
      </Grid>
    </>
  );
};

export default Profile;

Upvotes: 5

Views: 8903

Answers (3)

Hadi Mir
Hadi Mir

Reputation: 5133

If you want to setup the TOTP for user you have to call the AWS Cognito APIs in the following order

  1. Associate Software Token
  2. Verify Software Token
  3. Set User MFA Preference

The associate software token will give you an SecretCode which you will convert to a QR either so that user can scan it with an authenticator app. Then you will call the verify software token and pass it the code generated by the authenticator app. And finally you will enable the MFA by calling the set user preference API. And voila.

Upvotes: 4

fanartie
fanartie

Reputation: 1

You will see this MFA issue if you don't have phone_number ready.

Upvotes: 0

raulra08
raulra08

Reputation: 341

I got this error too and it took me a while to figure out what was wrong

My Cognito's configuration:

  • Using custom messages trigger lambda, users are invited to sign-up after they receive an email
  • TOTP is enabled (let's ignore this for now)
  • SMS is enabled
  • Attribute verification is set to No Verification

This is how I solved the issue:

First, I used AWS CLI to reproduce the steps to onboard users:

  1. Admin creates a new user
aws cognito-idp admin-create-user \
--user-pool-id <user-pool-id> \
--username <user_email> \
--user-attributes Name="email",Value="<user_email>" Name="name",Value="Alice" Name="family_name",Value="Doe" Name="email_verified",Value="true" \
--force-alias-creation \
--temporary-password "LongSecret132@" \
--desired-delivery-mediums "EMAIL" \
--profile <aws_config_profile>
  1. A CustomMessage_AdminCreateUser is triggered synchronouly, if lambda succeeds the user receives an email with a link to complete sing-up.

This is why I create users with --desired-delivery-mediums "EMAIL", above

  1. The frontend would route the user to the correct UI
  2. The user has to set a password, otherwise the user would remain in FORCE_CHANGE_PASSWORD status
aws cognito-idp admin-set-user-password --user-pool-id <user-pool-id> --username <user_email> --password "NewSecret@111" --permanent --profile <aws_config_profile>
  1. The user has to set a phone number. Done now because they aren't asked before step 1. If you want you could ask before and set the Name="phone_number",Value="<user_phone_number>" and Name="phone_number_verified",Value="true" in step 1.
aws cognito-idp admin-update-user-attributes \
--user-pool-id <user-pool-id> \
--username <user_email> \
--user-attributes Name="phone_number",Value="+447000200100" Name="phone_number_verified",Value="true" \
--profile <aws_config_profile>
  1. Sequencially, the user mfa prefence needs to be set before users can authenticate themselves.
aws cognito-idp admin-set-user-mfa-preference \
--user-pool-id <user-pool-id> \
--username <user_email> \
--sms-mfa-settings Enabled=true,PreferredMfa=true \
--profile <aws_config_profile>

So I did this all manually and then using the UI I tried to authenticate and Cognito did send me an SMS! And no error.

My conclusion is that the error happens when the user attributes (step 5) and mfa preference (step 6) arent setup properly and in the correct order.

You can try the commands above with a test user.

Second, Amplify docs were useful. The React code could do something like:

  1. Cosinder:
  • the api created the user (steps 1).
  • the user set their new password (steps 2 to 4)
  • the api registered the user's phone number (step 5)

All you are left with is handling the MFA either for the first time or after,

const fetchCognitoUser = async (flag = false) => {
  const user = await Auth.currentAuthenticatedUser({
    bypassCache: flag,
  });
  return user;
};

const handleMfaAuth = async ({ userObject, mfaToken }) => {
    try {
      // when MFA is being setup the first time
      // checks if this does not exist because
      // setting mfa to optional in cognito will not give you a challengeName
      if (!userObject.challengeName) {
        await Auth.verifyTotpToken(userObject, mfaToken);
        
        // This would be step 6 in the CLI version above.
        await Auth.setPreferredMFA(userObject, 'SMS');
        
        const cognitoUser = await fetchCognitoUser().catch((err) => {
          console.error(err);
        });

        if (cognitoUser) {
          // any logic to ensure correct Authentication
          // Load apps landing page.
        }
      }
      // when the user enters the MFA token
      else if (userObject.challengeName === 'SMS_MFA') {
        await Auth.confirmSignIn(userObject, mfaToken, 'SMS_MFA');
        const cognitoUser = await fetchCognitoUser().catch((err) => {
          console.error(err);
        });

        if (cognitoUser) {
          // any logic to ensure correct Authentication
          // Load apps landing page.
        }
      }
    } catch (error) {
      // Token is not verified
      setShowError(true);
    }
  };
  1. Say the user credentials are set but their MFA got deactivated (e.g manually in AWS Console) or they are simply performing log-in again.
const handleSignIn = async ({ username, password }) => {
  try{
    const user = await Auth.signIn(username, password);
    // if MFA hasn't been setup
    // checks if this does not exist because
    // setting mfa to optional in cognito will not give you a challengeName
    if (!user.challengeName) {
      // show the page to set up MFA
      setShowSetupMfa(true);
    } else if (user.challengeName === 'SMS_MFA') {
      // If MFA is enabled, sign-in should be confirmed with the confirmation code
      const loggedUser = await Auth.confirmSignIn(
          user,   // Return object from Auth.signIn()
          code,   // Confirmation code captured in the UI
          mfaType // MFA Type e.g. SMS_MFA, SOFTWARE_TOKEN_MFA
      );
      // show the page to enter mfa token
      setShowMfa(true);
    }
  } catch (error) {
    // handle error
  }
}

Hope this helps 🙌

Make sure Cognito is correctly configured. You need SMS enabled, the documentation explains how to do it

I must say that boto3's documentation helped me understand my options and how some things work in cognito, I definetly recommend taking a look for those of you having similar or slightly different issues.

Upvotes: 5

Related Questions