How to validate Cognito SMS_MFA code using AWS SDK in NodeJS?
Problem
I have a React login page. When a user enters a username/password, the request goes to the backend and the backend executes the signInUser method as shown below.
NOTE: If MFA is disabled for a user, it returns a set of tokens eg. AccessToken, RefreshToken, and IdToken, I send these tokens to the front end and the user then goes to the dashboard. In short, things work fine.
But if I enable MFA manually incognito for the same user, signInUser will send a challenge called "SMA_MFA" and it sends the 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 the user to a page where he can enter 6 digit code. Then clicking the verify button makes a call to the backend API/verify-mfa-code.
Now in the backend, I don't know how to verify the MFA Code....
What I have tried is as follows,
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); |
Answered by: >Ermiya Eskandary
Credit: >StackOverflow
Blog Links:
>How do I customize controller actions in Laravel Jetstream?
>Run Laravel Project to Active Local Server
>How to show encrypted user id in URL in Laravel?
>How to fix Laravel LiveWire listener?