1

my goal is to implement otp by sending a sms to user mobile. im able to achieve this using cognito custom auth flow, but, only works if the user success in the firts attemp, if the user enter a bad code, the session is gonna expire and a new code is required to be sent again, bad ux. i do need at least 3 attemps, which in theory are 3 sessions across this cognito auth flow.

im gonna share the four cognito lambdas (cognito triggers) i used for this: preSignUp, defineAuthChallenge, createAuthChallenge and verifyChanllenge

// preSignUp lambda
exports.handler = async (event) => {
    event.response.autoConfirmUser = true;
    event.response.autoVerifyPhone = true;
    return event;
};
// defineAuthChallenge
exports.handler = async (event, context, callback) => {    
    if (event.request.session.length >= 3 && event.request.session.slice(-1)[0].challengeResult === false) {
        // wrong OTP even After 3 sessions? FINISH auth, dont send token
        event.response.issueToken = false;
        event.response.failAuthentication = true;
    } else if (event.request.session.length > 0 && event.request.session.slice(-1)[0].challengeResult === true) { 
        // Last answer was Correct! send token and FINISH auth
        event.response.issueTokens = true;
        event.response.failAuthentication = false;
    } else { 
        // INIT flow - OR - not yet received correct OTP
        event.response.issueTokens = false;
        event.response.failAuthentication = false;
        event.response.challengeName = 'CUSTOM_CHALLENGE';
    }
    return event;
};
// createAuthChallenge
exports.handler = async (event, context) => {
    if (!event.request.session || event.request.session.length === 0) {
        // create only once the otp, send over sms only once
        var otp = generateOtp();
        const phone = event.request.userAttributes.phone_number;
        sendSMS(phone, otp);
    } else {
        // get previous challenge answer
        const previousChallenge = event.request.session.slice(-1)[0];
        otp = previousChallenge.challengeMetadata;
    }
    event.response = {
        ...event.response, 
        privateChallengeParameters: {
           answer: otp
        },
        challengeMetadata: otp // save it here to use across sessions
    };
    return event
}
// verifyChanllenge
exports.handler = async (event, context) => {
  event.response.answerCorrect = event.request.privateChallengeParameters.answer === event.request.challengeAnswer;
  return event
}

for the client, which is a RN app, im using amplify, this is the flow in the app:

// SignIn form screen
import { Auth } from "aws-amplify";

const signUp = (phone) => {
    Auth.signUp({
      username: phone,
      /** dummy pass since its required but unused for OTP */
      password: "12345678"
    }).then(() => {
      // after signup, go an automatically login (which trigger sms to be sent)
      otpSignIn(phone);
    }).catch(({code}) => {
      // signup fail because user already exists, ok, just try login it
      if (code === SignUpErrCode.USER_EXISTS) {
        otpSignIn(phone)
      } else {
        ...  
      }
    })
  }

const otpSignIn = async (phoneNumber) => {
    const cognitoUser = await Auth.signIn(phoneNumber)
    setCognitoUser(cognitoUser);
    navigate("ConfirmNumber", {phoneNumber});
  }
import { Auth } from "aws-amplify";
let cognitoUser;

export function setCognitoUser(user) {
  console.log('setCognitoUser', user)
  cognitoUser = user;
}

export function sendChallenge(challengeResponse) {
  return Auth.sendCustomChallengeAnswer(cognitoUser, challengeResponse)
}
// Confirm number screen
const onChangeText = (value) => {
    if (value.length === 4) {
      try {
        const user = await sendChallenge(value)
        // WEIRD THING NUMBER 1
        // when the user send the second attempt, no error is raised, this promise is resolve! 
        // even when the trigger *verifyChanllenge* is returning false.

      } catch (err) {
        // WEIRD THING NUMBER 2
        // from the trigger *createAuthChallenge* if i define the anser in the if block, 
        // and not store such answer for future use (i do that in else block), then, 
        // for the second..third attempt the error raised here is that *Invalid session for user* which mean session has expired, 
        // what i need is to persist session until third attempt 
      }
    }
}
// this is amplify config: try 1
const awsExports = {
  Auth: {
    region: ...,
    userPoolId: ...,
    userPoolWebClientId: ...,
    authenticationFlowType: 'CUSTOM_AUTH',
  },
  ...
}
Amplify.configure(awsExports);  
// this is amplify config: try 2
import {Auth} from "aws-amplify"
Auth.configure({
  authenticationFlowType: 'CUSTOM_AUTH'
});
adrian oviedo
  • 684
  • 1
  • 8
  • 27

1 Answers1

1

everything is correct in the code above, and the config for amplify authenticationFlowType: 'CUSTOM_AUTH' is not necessary.

the problem is that Auth.sendCustomChallengeAnswer(cognitoUser, challengeResponse) is not raising an error when the trigger defineAuthChallenge set this combination:

event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';

which presents the next attempt.

so i found a way to check the error when the user fail the otp:

const sendCode = async (value) => {
    try {
      // send the answer to the User Pool
      // this will throw an error if it's the 3rd wrong answer
      const user = await sendChallenge(value);

      // the answer was sent successfully, but it doesnt mean it is the right one
      // so we should test if the user is authenticated now
      // this will throw an error if the user is not yet authenticated:
      await Auth.currentSession();
    } catch (err) {
      setError(true);
    }
  }
adrian oviedo
  • 684
  • 1
  • 8
  • 27