I am working on implementing a passwordless authentication flow using Amazon Cognito Custom Authentication Challenges. I have three Lambda triggers set up: DefineAuthChallenge, CreateAuthChallenge, and VerifyAuthChallenge.
Here’s what I want to achieve:
1. Ask the user how they want to receive the OTP (via email or SMS). Since Cognito does not pass user input metadata to Lambda, I’m attempting to ask the user to select the delivery method in the first challenge, and then pass that selection as metadata to the next challenges.
2. Generate and validate the OTP based on the method selected in the first step, and grant the user access if the OTP is correct.
What Works:
If I have only one challenge (generate OTP and validate it), everything works fine. The OTP is sent, and if the user enters the correct one, they are granted access.
The Issue: When trying to implement multiple challenges:
On first call session is 0 so it calls challenge to select the OTP delivery method but doesn’t trigger the CreateAuthChallenge Lambda for OTP generation.
There’s also errors sometimes in the response saying it’s an unrecognized Lambda output or Local storage is missing an ID Token, Please authenticate cognito custom auth
DefineAuthLambda
import json
def lambda_handler(event, context):
print("EVENT request:", event["request"])
session = event["request"]["session"]
# First challenge: Ask the user how they want to receive the OTP
if len(session) == 0:
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "SELECT_DELIVERY_METHOD" # Metadata to show the selection UI
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
event["response"]["publicChallengeParameters"] = {
"question": "How would you like to receive the OTP? (email or sms)"
}
# Second challenge: After the user selects delivery method, send OTP
elif len(session) == 1 and session[0]["challengeName"] == "CUSTOM_CHALLENGE" and session[0]["challengeResult"]:
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "VERIFY_OTP" # Metadata for verifying OTP
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
event["response"]["publicChallengeParameters"] = {
"otpPrompt": "Enter the OTP sent to your email or phone"
}
# Third challenge: If OTP is correct, issue tokens
elif len(session) == 2 and session[1]["challengeName"] == "CUSTOM_CHALLENGE" and session[1]["challengeResult"]:
event["response"]["issueTokens"] = True
event["response"]["failAuthentication"] = False
# Failure case: If authentication fails
else:
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = True
print("EVENT response:", event["response"])
return event
CreateAuthLambda
import json
import random
def lambda_handler(event, context):
print("EVENT request:", event["request"])
session = event["request"]["session"]
# First challenge: Ask the user how they want to receive the OTP
if len(session) == 0:
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "SELECT_DELIVERY_METHOD" # This indicates the frontend to display method selection
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
event["response"]["publicChallengeParameters"] = {
"question": "How would you like to receive the OTP? (email or sms)"
}
# Second challenge: After the user selects delivery method, send OTP
elif len(session) == 1 and session[0]["challengeName"] == "CUSTOM_CHALLENGE" and session[0]["challengeResult"]:
# Get the delivery method selected by the user in the UI (email or sms)
selected_method = event["request"]["challengeAnswer"]
# Generate a random 8-digit OTP
otp = str(random.randint(10000000, 99999999))
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "VERIFY_OTP" # Indicating the frontend to ask for OTP verification
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
# Store OTP and the selected delivery method in privateChallengeParameters
event["response"]["privateChallengeParameters"] = {
"otp": otp,
"deliveryMethod": selected_method # Store selected method (either email or sms)
}
# Simulate sending OTP to the selected method (email or sms)
print(f"Sending OTP {otp} via {selected_method}")
# Third challenge: If OTP is correct, issue tokens
elif len(session) == 2 and session[1]["challengeName"] == "CUSTOM_CHALLENGE" and session[1]["challengeResult"]:
event["response"]["issueTokens"] = True
event["response"]["failAuthentication"] = False
# Failure case: If authentication fails
else:
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = True
print("EVENT response:", event["response"])
return event
VerifyAuthLambda
def lambda_handler(event, context):
print("EVENT request:", event["request"])
challenge_name = event["request"]["challengeName"]
user_answer = event["request"]["challengeAnswer"]
private_params = event["request"]["privateChallengeParameters"]
# If the challenge is for OTP delivery method selection (first challenge)
if challenge_name == "CUSTOM_CHALLENGE":
if event["request"]["challengeMetadata"] == "CHOOSE_OTP_DELIVERY":
# No OTP to verify yet, just handle the user's delivery method choice
selected_method = user_answer # 'email' or 'sms' (based on the user's choice)
event["response"]["answerCorrect"] = True # Mark as correct, as it's just delivery method selection
# If the challenge is for OTP verification (second challenge)
elif event["request"]["challengeMetadata"] == "VERIFY_OTP":
expected_otp = private_params["otp"] # OTP sent to the user (either email or SMS)
event["response"]["answerCorrect"] = (user_answer == expected_otp)
print("EVENT response:", event["response"])
return event
How can I present both challanges to the user without errors?
I am working on implementing a passwordless authentication flow using Amazon Cognito Custom Authentication Challenges. I have three Lambda triggers set up: DefineAuthChallenge, CreateAuthChallenge, and VerifyAuthChallenge.
Here’s what I want to achieve:
1. Ask the user how they want to receive the OTP (via email or SMS). Since Cognito does not pass user input metadata to Lambda, I’m attempting to ask the user to select the delivery method in the first challenge, and then pass that selection as metadata to the next challenges.
2. Generate and validate the OTP based on the method selected in the first step, and grant the user access if the OTP is correct.
What Works:
If I have only one challenge (generate OTP and validate it), everything works fine. The OTP is sent, and if the user enters the correct one, they are granted access.
The Issue: When trying to implement multiple challenges:
On first call session is 0 so it calls challenge to select the OTP delivery method but doesn’t trigger the CreateAuthChallenge Lambda for OTP generation.
There’s also errors sometimes in the response saying it’s an unrecognized Lambda output or Local storage is missing an ID Token, Please authenticate cognito custom auth
DefineAuthLambda
import json
def lambda_handler(event, context):
print("EVENT request:", event["request"])
session = event["request"]["session"]
# First challenge: Ask the user how they want to receive the OTP
if len(session) == 0:
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "SELECT_DELIVERY_METHOD" # Metadata to show the selection UI
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
event["response"]["publicChallengeParameters"] = {
"question": "How would you like to receive the OTP? (email or sms)"
}
# Second challenge: After the user selects delivery method, send OTP
elif len(session) == 1 and session[0]["challengeName"] == "CUSTOM_CHALLENGE" and session[0]["challengeResult"]:
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "VERIFY_OTP" # Metadata for verifying OTP
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
event["response"]["publicChallengeParameters"] = {
"otpPrompt": "Enter the OTP sent to your email or phone"
}
# Third challenge: If OTP is correct, issue tokens
elif len(session) == 2 and session[1]["challengeName"] == "CUSTOM_CHALLENGE" and session[1]["challengeResult"]:
event["response"]["issueTokens"] = True
event["response"]["failAuthentication"] = False
# Failure case: If authentication fails
else:
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = True
print("EVENT response:", event["response"])
return event
CreateAuthLambda
import json
import random
def lambda_handler(event, context):
print("EVENT request:", event["request"])
session = event["request"]["session"]
# First challenge: Ask the user how they want to receive the OTP
if len(session) == 0:
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "SELECT_DELIVERY_METHOD" # This indicates the frontend to display method selection
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
event["response"]["publicChallengeParameters"] = {
"question": "How would you like to receive the OTP? (email or sms)"
}
# Second challenge: After the user selects delivery method, send OTP
elif len(session) == 1 and session[0]["challengeName"] == "CUSTOM_CHALLENGE" and session[0]["challengeResult"]:
# Get the delivery method selected by the user in the UI (email or sms)
selected_method = event["request"]["challengeAnswer"]
# Generate a random 8-digit OTP
otp = str(random.randint(10000000, 99999999))
event["response"]["challengeName"] = "CUSTOM_CHALLENGE"
event["response"]["challengeMetadata"] = "VERIFY_OTP" # Indicating the frontend to ask for OTP verification
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = False
# Store OTP and the selected delivery method in privateChallengeParameters
event["response"]["privateChallengeParameters"] = {
"otp": otp,
"deliveryMethod": selected_method # Store selected method (either email or sms)
}
# Simulate sending OTP to the selected method (email or sms)
print(f"Sending OTP {otp} via {selected_method}")
# Third challenge: If OTP is correct, issue tokens
elif len(session) == 2 and session[1]["challengeName"] == "CUSTOM_CHALLENGE" and session[1]["challengeResult"]:
event["response"]["issueTokens"] = True
event["response"]["failAuthentication"] = False
# Failure case: If authentication fails
else:
event["response"]["issueTokens"] = False
event["response"]["failAuthentication"] = True
print("EVENT response:", event["response"])
return event
VerifyAuthLambda
def lambda_handler(event, context):
print("EVENT request:", event["request"])
challenge_name = event["request"]["challengeName"]
user_answer = event["request"]["challengeAnswer"]
private_params = event["request"]["privateChallengeParameters"]
# If the challenge is for OTP delivery method selection (first challenge)
if challenge_name == "CUSTOM_CHALLENGE":
if event["request"]["challengeMetadata"] == "CHOOSE_OTP_DELIVERY":
# No OTP to verify yet, just handle the user's delivery method choice
selected_method = user_answer # 'email' or 'sms' (based on the user's choice)
event["response"]["answerCorrect"] = True # Mark as correct, as it's just delivery method selection
# If the challenge is for OTP verification (second challenge)
elif event["request"]["challengeMetadata"] == "VERIFY_OTP":
expected_otp = private_params["otp"] # OTP sent to the user (either email or SMS)
event["response"]["answerCorrect"] = (user_answer == expected_otp)
print("EVENT response:", event["response"])
return event
How can I present both challanges to the user without errors?
Share Improve this question edited Mar 25 at 3:27 luk2302 57.3k24 gold badges104 silver badges149 bronze badges asked Mar 25 at 3:16 burns0907burns0907 1362 silver badges9 bronze badges1 Answer
Reset to default 0In DefineAuthChallenge, the initial request starts with a session length of 0. At this stage, we present the first challenge to determine the user’s preferred OTP delivery method (Email or SMS). Once this challenge is successfully completed, we proceed to the second challenge for OTP verification.
As shown, the session length increases after each challenge, allowing us to add multiple challenges as needed before issuing the final authentication tokens.
if session_length == 0:
# First authentication attempt, issue custom challenge
event['response']['challengeName'] = 'CUSTOM_CHALLENGE'
event['response']['issueTokens'] = False
event['response']['failAuthentication'] = False
elif session_length == 1 and session[-1].get('challengeResult') is True:
# If the last challenge was successful, issue tokens
event['response']['challengeName'] = 'CUSTOM_CHALLENGE'
event['response']['issueTokens'] = False
event['response']['failAuthentication'] = False
elif session_length == 2 and session[-1].get('challengeResult') is True:
# If the last challenge was successful, issue tokens
event['response']['issueTokens'] = True
event['response']['failAuthentication'] = False
else:
# If challenge failed, authentication should fail
event['response']['issueTokens'] = False
event['response']['failAuthentication'] = True
print("Final response:", json.dumps(event['response'], indent=2))
return event
In CreateAuthChallenge, we use the session length to determine which challenge. The client passes metadata (such as the user’s input from the UI) through clientMetadata, which helps shape and respond to each challenge appropriately.
if session_length == 0 and event['request']['challengeName'] == 'CUSTOM_CHALLENGE':
# First challenge: Ask user how they want to receive OTP
event['response']['publicChallengeParameters'] = {
"question": "Do you want to receive the OTP via Email or Phone?"
}
event['response']['privateChallengeParameters'] = {"answer": "email_or_phone"}
event['response']['challengeMetadata'] = 'SELECT_OTP_METHOD'
return event
if session_length == 1 and event['request']['challengeName'] == 'CUSTOM_CHALLENGE':
# Second challenge: Generate and send OTP
delivery_method = event['request']['clientMetadata']['login_method']
print("Delivery method:", delivery_method)
otp = str(random.randint(10000000, 99999999)) # Generating a 8-digit OTP
if delivery_method == 'EMAIL_OTP':
# send email
if delivery_method == 'SMS_OTP':
# send sms