title: AWS Cognito Software Token MFA works once, then unexpectedly reverts to SMS MFA for all future logins
body: <h2>Background:</h2>
  <p>I have a React web app (utilizing <code>aws-amplify</code>) which is connecting to/using an AWS Cognito User Pool for auth.</p>
  <p>I am trying to enable MFA and, more specifically, I want my users to have the option to utilize Software Token TOTP MFA (i.e. Google Authenticator, or similar, app).</p>
  <p>When I set my User Pool to have MFA required, I am forced to enable SMS MFA and then Software TOTP is optional. In my case, I have TOTP enabled.</p>
  <p>In my web app, I have added the necessary component via:</p>
  <pre class="lang-js prettyprint-override"><code>import { SelectMFAType } from 'aws-amplify-react';
  
  # other code
  
  &lt;SelectMFAType authData={user} MFATypes={{ SMS: true, TOTP: true }} /&gt;
  
  # other code
  </code></pre>
  <p>If you're unfamiliar with <code>aws-amplify-react</code> and/or <code>SelectMFAType</code>, this component provides a UI element where the user can choose if they prefer to use SMS or Software TOTP as their MFA method. If they choose SMS, their previously-verified phone number is used and everything works.</p>
  <p>If the user chooses TOTP, they are shown a QR code to scan in the authenticator app of their choice and they are prompted with an input field to enter a 6-digit number from the authenticator app to verify TOTP.
  This is all very standard for anyone who has used a TOTP MFA option on any other website. If the user enters a correct code from the app, their TOTP choice is verified.</p>
  <p>In short <code>SelectMFAType</code> is just a shortcut/stand-in to quickly prototype and test without needing to create a custom component.</p>
  <h2>Problem:</h2>
  <p>Now, here's the problem and how to reproduce it. (The starting point is a user who has just enabled TOTP.):</p>
  <ol>
  <li>User logs out.</li>
  <li>User logs in.</li>
  <li>If correct username/password, user is prompted for TOTP.</li>
  <li>If correct TOTP, user is logged in. <strong>This is working perfectly so far, but it won't stay that way.</strong></li>
  <li>User logs out.</li>
  <li>User logs in.</li>
  <li>If correct username/password, user is prompted for SMS MFA and will receive a text message with 6-digit code. <strong>This is the unexpected behavior. I expect it to continue requesting TOTP MFA from their authenticator app unless the user changes their preferred method back to SMS.</strong></li>
  <li>From this point forward, the user will only ever be asked for SMS MFA.</li>
  </ol>
  <p>In between step 4 and step 7, the user's preference was not changed. Absolutely nothing changed in the React app or in the AWS User Pool settings.</p>
  <p>Further, if I interrogate the user via the AWS CLI command
  <code>aws cognito-idp admin-get-user --user-pool-id ${MY_ID} --username ${MY_USER_NAME}</code>, I can confirm that the user's MFA preference is exactly what I expect:</p>
  <pre><code>{
      &lt;other irrelevant keys redacted&gt;
      &quot;PreferredMfaSetting&quot;: &quot;SOFTWARE_TOKEN_MFA&quot;,
      &quot;UserMFASettingList&quot;: [
          &quot;SMS_MFA&quot;,
          &quot;SOFTWARE_TOKEN_MFA&quot;
      ]
  }
  </code></pre>
  <p>The MFA challenge is an API response from AWS Cognito when the user attempts to authenticate and it is either <code>SMS_MFA</code> or <code>SOFTWARE_TOKEN_MFA</code>. In my case, I get a single <code>SOFTWARE_TOKEN_MFA</code> challenge, but then all future challenges revert, against my wishes, to <code>SMS_MFA</code>.</p>
  <p>If I repeat the TOTP setup process (delete the entry from authenticator app, re-verify, etc.), I can repeat all of the steps again. By that, I mean the MFA will expect TOTP one time and then again revert back to SMS after that first time.</p>
  <p>Can anyone shed light on this situation? Have you experienced it? Is it a known issue/bug in AWS Cognito? Am I doing something wrong? I feel like if this was broken, there would be a pretty big amount of noise being generated, but I can't find anyone else with the same issue.</p>
  <h2>Things I have tried:</h2>
  <ul>
  <li>Searched extensively for anyone else with the same issue here, Google, and the AWS forums.</li>
  <li>I have posted on the AWS forums, but that has gone nowhere:  <a href="https://forums.aws.amazon.com/thread.jspa?threadID=324131" rel="nofollow noreferrer">https://forums.aws.amazon.com/thread.jspa?threadID=324131</a></li>
  <li>Created a custom component to replace <code>SelectMFAType</code> under the theory that there was something wrong with the implementation in <code>aws-amplify-react</code>. I will paste that custom component at the bottom of this question as a reference.</li>
  <li>Completely destroyed and re-created the AWS Cognito User Pool. I thought perhaps it was corrupted or otherwise not working. This made no difference.</li>
  </ul>
  <h2><code>SetupTOTP.js</code> Component:</h2>
  <pre class="lang-js prettyprint-override"><code>import React, { useContext, useEffect, useState } from 'react';
  import {
    Button,
    Dialog,
    DialogActions,
    DialogContent,
    DialogContextText,
    DialogTitle,
    TextField,
  } from '@material-ui/core';
  import { Auth } from 'aws-amplify';
  import { ToastsStore } from 'react-toasts';
  import QRCode from 'qrcode.react';
  
  import { AuthStateContext } from 'Context/auth-context';
  
  const SetupTOTP = React.memo(props =&gt; {
    const { open, handleClose } = props;
    const { username } = useContext(AuthStateContext);
    const [user, setUser] = useState(null);
    const [qrCode, setQrCode] = useState('');
    const [token, setToken] = useState('');
  
    const handleSave = () =&gt; {
      Auth.verifyTotpToken(user, token)
        .then(() =&gt; {
          Auth.setPreferredMFA(user, 'TOTP').then(() =&gt; {
            ToastsStore.success('Token Verified Updated');
            handleClose();
          });
        })
        .catch(err =&gt; {
          console.log(err);
          ToastsStore.error(err.message);
        });
    };
  
    useEffect(() =&gt; {
      Auth.currentAuthenticatedUser().then(user =&gt; {
        setUser(user);
      });
    }, []);
  
    useEffect(() =&gt; {
      if (username &amp;&amp; user) {
        Auth.setupTOTP(user).then(code =&gt; {
          setQrCode(
            `otpauth://totp/AWSCognito:${username}?secret=${code}&amp;issuer=REDACTED`
          );
        });
      }
    }, [username, user]);
  
    return (
      &lt;Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby=&quot;form-dialog-title&quot;
      &gt;
        &lt;DialogTitle id=&quot;form-dialog-title&quot;&gt;Change Password&lt;/DialogTitle&gt;
        &lt;DialogContent&gt;
          &lt;QRCode value={qrCode} /&gt;
  
          &lt;TextField
            margin=&quot;dense&quot;
            id=&quot;name&quot;
            label=&quot;Verify Token&quot;
            type=&quot;text&quot;
            fullWidth
            onChange={e =&gt; setToken(e.target.value)}
          /&gt;
        &lt;/DialogContent&gt;
        &lt;DialogActions&gt;
          &lt;Button onClick={handleClose} color=&quot;primary&quot;&gt;
            Cancel
          &lt;/Button&gt;
          &lt;Button onClick={handleSave} color=&quot;primary&quot;&gt;
            Save
          &lt;/Button&gt;
        &lt;/DialogActions&gt;
      &lt;/Dialog&gt;
    );
  });
  
  export default SetupTOTP;
  </code></pre>