Search code examples
node.jsexpressamazon-cognitoaws-sdk-jsmulti-factor-authentication

Validate Cognito SMS_MFA code using aws sdk in Nodejs


I have react login page. When user enters username/password, request goes to backend and backend executes signInUser method as shown below.


NOTE: If MFA is disabled for a user, it returns a set of tokens eg. AccessToken, RefreshToken, IdToken, I send these tokens to front-end and user then goes to dashboard. In short things work fine.

But if I enable MFA manually in cognito for the same user, signInUser will send a challenge called "SMA_MFA" and it sends following object,

{
    "ChallengeName": "SMS_MFA",
    "Session": "AYABeCMrhuH....",
    "ChallengeParameters": {
        "CODE_DELIVERY_DELIVERY_MEDIUM": "SMS",
        "CODE_DELIVERY_DESTINATION": "+91*******83",      //<<<<<<--------- I receive 6 digit code on my mobile with this response
        "USER_ID_FOR_SRP": "userA"
    }
}

Cognito.Service.js

this.cognitoIdentity = new AWS.CognitoIdentityServiceProvider(this.config);

async signInUser(username, password, cb) {
    
    const params = {
      AuthFlow: "USER_PASSWORD_AUTH",
      ClientId: this.clientId,
      AuthParameters: {
        USERNAME: username,
        PASSWORD: password,
        SECRET_HASH: this.generateHash(username)
      }
    };

    try {
      this.cognitoIdentity.initiateAuth(params, (err, data) => {
        if (err) {
          console.log("signinUser: error", err);
        } 
        else {
          
           if (data?.ChallengeName === "SMS_MFA") {  //<--------- checking if SMA_MFA challenge, sending entire response object back to FE...
                return res.status(200).send(data);
            }

          console.log(data)
        }
      });
    } catch (error) {
      console.log(error);
      return false;
    }
  }

Now, after receiving SMA_MFA challenge object in FE, I redirect user to a page where he can enter 6 digit code. Then clicking verify button makes a call to backend api/verify-mfa-code.

Now in backend, I don't know how to verify MFA Code....

What I have tried as follow,

async verifyMFACode(payload, cb) {
    try {
      const params = {
        Session: payload.Session,           //<<<<<-------- received session (token) that I received from FE (sent by backend to FE) as explained above
        UserCode: payload.userCode          //<<<<<-------- received 6 digit SMS code sent by FE
      }

      
      this.cognitoIdentity.verifySoftwareToken(params, (err, data) => {  //<<<<---- I don't know if I have to user verifySoftwareToken but tried with it.
        if (err) {
          console.log("refreshtoken initiateAuth error:", err)
          cb(err);
        } // an error occurred
        else {
          cb(null, data); // successful response
        }
      });
    } catch (err) {
      cb(err);
    }
  }

I get below error with verifySoftwareToken method,

NotAuthorizedException: Invalid session for the user.

I really have no idea how to answer SMA_Challenge using Nodejs using aws-sdk.

NOTE: Somewhere people are sending AccessToken in verifySoftwareToken params but I don't get AccessToken because user is yet not signedIn and he has to pass the challenge first.


Solution

  • The VerifySoftwareToken API is for TOTP MFA, not for SMS MFA.

    You need to use the RespondToAuthChallenge API () for validating SMS MFA codes.


    For v2 of the JS SDK, here is a minimal example using the respondToAuthChallenge method:

    var AWS = require('aws-sdk');
    
    var client = new AWS.CognitoIdentityServiceProvider();
    
    const userPoolAppClientId = "";
    const username = "";
    const mfaCode = "";
    const session = "";
    
    var request = {
        ChallengeName: 'SMS_MFA', 
        ClientId: userPoolAppClientId,
        ChallengeResponses: {
            'USERNAME': username,
            'SMS_MFA_CODE': mfaCode,
            'SECRET_HASH': this.generateHash(username)
        },
        Session: session
    };
    
    client.respondToAuthChallenge(request, function(err, response) {
        if (err) console.log(err, err.stack);
        else {
            var authenticationResult = {
                accessToken: response.AuthenticationResult.AccessToken,
                idToken: response.AuthenticationResult.IdToken,
                refreshToken: response.AuthenticationResult.RefreshToken,
            }
    
            console.log(authenticationResult);
        }
    });
    

    For v3 of the JS SDK, you need to use the RespondToAuthChallengeCommand JS SDK command.

    Here's the same minimal example, compatible with v3:

    import {
        CognitoIdentityProviderClient,
        RespondToAuthChallengeCommand
    } from "@aws-sdk/client-cognito-identity-provider";
    
    const client = new CognitoIdentityProviderClient(config);
    
    const userPoolAppClientId = "";
    const username = "";
    const mfaCode = "";
    const session = "";
    
    const request = {
        ChallengeName: "SMS_MFA",
        ClientId: userPoolAppClientId,
        ChallengeResponses: {
            "USERNAME": username,
            "SMS_MFA_CODE": mfaCode,
            'SECRET_HASH': this.generateHash(username)
        },
        Session: session,
    };
    
    const command = new RespondToAuthChallengeCommand(request);
    
    const response = await client.send(command);
    
    const authenticationResult = {
        accessToken: respondToAuthChallengeResponse.AuthenticationResult.AccessToken,
        idToken: respondToAuthChallengeResponse.AuthenticationResult.IdToken,
        refreshToken: respondToAuthChallengeResponse.AuthenticationResult.RefreshToken,
    }
    
    console.log(authenticationResult);