Search code examples
javascriptamazon-web-servicessdkamazon-cognitomulti-factor-authentication

Flow for authentication when MFA required for user in AWS Cognito


I am attempting to add MFA for user authentication to an already existing solution (built in Angular) for device management within AWS Cognito.

I am having trouble figuring out how to handle this particular response well from a user-experience perspective. It actually feels broken, so would love if anyone else has experience pain points here.

See Use Case 23. for example implementation, mine is below:

authenticate(username: string, password: string): Observable<any> {

    // init cognitoUser here

    return new Observable((observer) => {
        cognitoUser.authenticateUser(authenticationDetails, {
            onSuccess: (result: any) => {},
            onFailure: (err: Error) => {},
            mfaRequired: (codeDeliveryDetails: any) => {

                // SMS has just been sent automatically 
                // and it needs to be confirmed within this scope

                // The example linked requests the code via `confirm()`
                // which is awful UX...and since this is a service
                // probably non-compliant with best practice
                // However, without this `confirm` at this point in                     
                // time, we have no confirmationCode below

                cognitoUser.sendMFACode(confirmationCode, {
                    onSuccess: (result) => {
                        observer.next(result);
                        observer.complete();
                    }, onFailure: (err: Error) => {
                        observer.error(err);
                        observer.complete();
                    }
                });
            }
        });
    });
}

Expected:

  • If the user authenticates successfully but has not added this device through MFA, we can manage the redirect to appropriate confirmation code form page and trigger the sendMFACode function manually (perhaps through some sort of limited session?)

Issue/s:

  • we don't have a session, so we have no way of asking the user the MFA code sent automatically outside of this login screen...catch 22?
  • adding another show/hide field in the login form doesn't work as it would hit the sendMfaCode function multiple times, resulting in multiple SMS codes sent.

Has anyone had any luck stepping out of this flow?


Solution

  • Whilst I’m sure very talented people worked on the amazon-cognito-identity-js API, it is just straight up badly designed. Thus why it’s been depricated. My personal advise would be to migrate to Amplify, which makes me much less angry.

    With Amplify you can do these ones.



    import Amplify from 'aws-amplify'
    import Auth from '@aws-amplify/auth'
    
    let mfaRequired = false
    
    Amplify.configure({
        Auth: {
            userPoolWebClientId: '',
            userPoolId: ''
        }
    })
    
    const logUserIn = (user) => {
      // Go forth and be happy
    }
    
    // Run me on your login form's submit event
    const login = async (username, password) => {
      const user = await Auth.signIn(username, password)
    
      if (user.challengeName === 'SMS_MFA') {
        // Change UI to show MFA Code input
        mfaRequired = true
        return
      }
      return logUserIn(user)
    }
    
    // Run me when the user submits theire MFA code
    const senfMfaCode = async (mfaCode) => {
      const user = await Auth.confirmSignIn(mfaCode)
      return logUserIn(user)
    }
    

    BUT if for some sad reason you need to keep using amazon-cognito-identity-js don’t worry. I got you.

    Just keep the cognitoUser object stored outside the callback. The documentation is a little misleading because it only show’s self contained examples but there’s no reason that you can’t notify your UI when MFA is required and then call cognitoUser.sendMFACode() later.

    Just remember that the documentation show’s the passing of this to sendMFACode() for scoping (which is terrible) but you can just declare your callbacks as a variable and share it between your authenticateUser() and sendMFACode() functions (or as many functions as you like).

    import { CognitoUserPool, AuthenticationDetails, CognitoUser } from 'amazon-cognito-identity-js'
    
    export let mfaRequired = false
    export let cognitoUser = null
    
    export const cognitoCallbacks = {
      mfaRequired () {
        // Implement you functionality to show UI for MFA form
        mfaRequired = true
      },
      onSuccess (response) {
        // Dance for joy the code gods be glorious.
      },
      onFailure () {
        // Cry.
      }
    }
    
    export const logUserIn = payload => {
      cognitoUser = new CognitoUser({
        Username: 'Matt Damon',
        Pool: new CognitoUserPool({
          UserPoolId: '',
          ClientId: ''
        })
      })
      return cognitoUser.authenticateUser(new AuthenticationDetails(payload), cognitoCallbacks)
    }
    
    export const sendMfaCode = MFACode => {
      cognitoUser.sendMFACode(MFACode, cognitoCallbacks)
    }
    

    That’s a super basic implementation and on top of that you could,

    1. Just overwrite the mfaRequired function in an external module to do whatever you want.
    2. Wrap the whole thing in a pub/sub plugin and subscribe to events.

    Hope that helps!