AWS Cognito: Custom Challenge with Retry
Asked Answered
W

2

8

I am using Custom Challenge for MFA because I wanted to use Twilio instead of AMAZON SNS. I have successfully implemented it. It works fine but When a user enters the wrong OTP code. The user session is expired. means that he has to again provide a phone number and request an OTP again. Whereas I want it to retry at least 3 times. before he needs to request another OTP. My Response verify trigger is as simple as below, is there something that we can do.

(event, context, callback) => {
    if (event.request.privateChallengeParameters.answer == event.request.challengeAnswer) {
        event.response.answerCorrect = true;
    } else {
        event.response.answerCorrect = false;
    }
    callback(null, event);
}
Weitman answered 5/6, 2018 at 5:11 Comment(0)
Q
15

I acheived this by adding the answer as a variable into challengeMetaData - which so far as I can see is not returned to the client but is available on subsequent calls, I also have a variable named attempts to track how many times the user has entered an incorrect value.My code is below - I hope it helps

const AWS = require("aws-sdk");
exports.handler = (event, context, callback) => {

    const session = event.request.session;
    const currentSession = session ? session.length - 1 : 0

    switch (event.triggerSource) {
        case 'DefineAuthChallenge_Authentication':

            console.log("DefineAuthChallenge_Authentication");
            console.log(event);

            if (session.length === 0) {
                event.response = {
                    challengeName: 'CUSTOM_CHALLENGE',
                    failAuthentication: false,
                    issueTokens: false
                };
            }
            else {
                if (session[currentSession].challengeName === 'CUSTOM_CHALLENGE') {

                    if (session[currentSession].challengeResult === true) {
                        event.response.issueTokens = true;
                        event.response.failAuthentication = false;
                    }
                    else {

                        let metaData = JSON.parse(session[currentSession].challengeMetadata);
                        if (metaData.attempts <= 3) {
                            event.response = {
                                challengeName: 'CUSTOM_CHALLENGE',
                                failAuthentication: false,
                                issueTokens: false
                            };
                        }
                        else {
                            event.response.issueTokens = false;
                            event.response.failAuthentication = true;
                        }
                    }
                }
            }
            console.log(event);
            break;
        case 'CreateAuthChallenge_Authentication':
            if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
                console.log("CreateAuthChallenge_Authentication");
                console.log(event);
                if (session.length === 0) {
                    let answer = Math.random().toString(10).substr(2, 6);

//Your logic to send a message goes here
                    
                    event.response.publicChallengeParameters = { challengeType: 'SMS_CODE' };
                    event.response.privateChallengeParameters = { answer: answer };
                    event.response.challengeMetadata = JSON.stringify({ '_sid': answer, 'challengeType': 'SMS_CODE', attempts: 1 });
                }
                else {
                    let metaData = JSON.parse(session[currentSession].challengeMetadata);
                    if (metaData.attempts <= 3) {
                        event.response.publicChallengeParameters = { challengeType: 'SMS_CODE', errorCode: 'NotAuthorizedException' };
                        event.response.privateChallengeParameters = { answer: metaData._sid };
                        event.response.challengeMetadata = JSON.stringify({ '_sid': metaData._sid, 'challengeType': 'SMS_CODE', attempts: metaData.attempts + 1 });
                    }
                }
            }
            console.log(event);
            break;
        default:
            console.log("VerifyAuthChallenge_Authentication");
            console.log(event);
            if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) {
                event.response.answerCorrect = true;
            }
            else { event.response.answerCorrect = false; }
            console.log(event);
            break;
    }
    callback(null, event);
};
Quinones answered 7/8, 2018 at 0:1 Comment(7)
I see what you tried doing there. However, for some reason my second attempt is still failing. After one invalid attempt, my session is expired and I keep getting Invalid session for user error. The code that you have in Define Auth Challenge when otp attempt less than 3 is not getting invoked for me... Any suggestions ?Foy
Never mind...I figured it out. Your solution however was a great help. Thanks dude.Foy
@SaurabhTiwari what issue did you resolve? I'm having the same issue, after one attempt, I get Invalid session for user.Ichor
@TurbutAlin: The key is to reinitiate the session after the first wrong attempt. If you observe closely, the event.session is an array which keeps on getting multiple session as they expire. You can re-initaite a new session and run the whole flow internally again. The only downside however, is that when the session is re-initated in the same cycle you get a new session object and no error object explictly.Foy
In the above code from @Brett, notice when he compare attempts < 3 in DefineAuthChallenge, he initiates the session again by setting a CUSTOM_CHALLENGEFoy
Probably an interesting read: Implementing passwordless email authentication with Amazon Cognito where the solution is similar but somewhat simpler as the retry/attempt behaviour is limited to the Define Auth Challenge.Dittography
I had this problem too and its fixed now. To elaborate... The session string will change after each failed code entered. But the verification code stays the same throughout all chained CUSTOM_CHALLENGE events. Once I sent in the session string returned by the last response from RespondToAuthChallenge, I stopped getting the Invalid session for user error and was able to generate successfully on a retry.Randi
F
0

For anyone using CognitoIdentityProviderClient(@aws-sdk/client-cognito-identity-provider).

A new session is created after an invalid otp/answer is issued. A second attempt should use the new session otherwise it will fail authentication.

async verifyOTP({ username, otp, session }) {
const input: RespondToAuthChallengeCommandInput = {
  ClientId: process.env.COGNITO_CLIENT_ID,
  ChallengeName: ChallengeNameType.CUSTOM_CHALLENGE,
  Session: session,
  ChallengeResponses: {
    ANSWER:  otp,
    USERNAME: username,
  },
};
try {
  const command = new RespondToAuthChallengeCommand(input);
  const response = await this.client.send(command);
  // sent to client after failed attempt
  if (!response.AuthenticationResult) {
    const { Session, ChallengeParameters } = response;
    return {
      Session,
      attempts: ChallengeParameters.attempts,
      attemptsLeft: ChallengeParameters.attemptsLeft,
    };
  }
  return response.AuthenticationResult;
} catch (error) {
  throw new ForbiddenException("Incorrect  OTP");
}

}

On the client side need to retrieve new session and call verify otp using the new session i.e verifyOTP({username:'test', otp:'1234', session: newSession}).

see also cognito lambda triggers here : cognito lambda triggers gist

Fiche answered 23/1 at 14:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.