Password Less Authentication in Cognito

Neeraj Dhiman
DigiCred
Published in
6 min readApr 11, 2020

--

Recently I was working on AWS-Cognito to move out Products authN to Cognito User Pools.

Understanding Cognito, how it works and moving authN to User Pools was not that complex. Username/password and Login via social accounts was easily mapped with cognito Username/Password auth and federal identity auth. But our product also offer OTP based authN and to map this flow in cognito was not that straightforward.

I read official docs/blogs but while implementation I still faced edge cases which were not covered in docs or blogs and needed some extra digging on GitHub issues or Users Blogs who had implemented this. Also none of the blog were using Python (however pipeline remains the same) so I decided to write down this story, discuss about the challenges I faced and their solutions.

I was using Boto3 for the aws-cognito communication. Boto3 documentation is quite good but still it have missed some information like

  • While submitting Custom auth Challenge Request it doesn’t tell in which key you have submit your answer. If you will submit the same in PASSWORD key ( as in username/password) then its in-correct. For custom auth you have to use ANSWER

{
‘USERNAME’:username,
‘ANSWER’: answer
}

  • Every Trigger required in Custom Auth pipeline (I’ll explain these triggers in detail) works in per-defined Request-Response and any invalid key in Response will raise 500 internal server error.

In this doc we will discuss how OTP based Authentication Pipeline in Cognito can be setup.

We will be using Amazon Cognito user pools with Custom Authentication Challenge Lambda Triggers.

Custom Auth workflow

Complete Custom Auth flow follows following steps :

  1. The user enters their contact details (email/phone number) on the sign-up/sign-in page, our signIn function combine this request with CUSTOM_AUTH auth_flow and sends it to your Amazon Cognito user pool.
  2. The user pool receives the call and finds out that auth_flow is CUSTOM_AUTH and therefore calls the “Define Auth Challenge” Lambda function. This Lambda function determines which custom challenge needs to be created.
  3. The user pool calls the “Create Auth Challenge” Lambda function. This Lambda function generates a secret login code (you have to create your own) and sends this code to the user’s device via SMS or Email depends on the contact (using Amazon SNS as the transport service OR you can use your own service)
  4. The user retrieves the secret login code on their device and enters it into the custom sign-in page, which is then sent back and your user pool calls the “Verify Auth Challenge Response” Lambda function to verify the users’ response.
  5. The user pool calls the “Define Auth Challenge” Lambda function again to verify that the challenge has been successfully answered and that no further challenge is needed. Function returns “issueTokens: true” in its response to the user pool. The user pool now considers the user to be authenticated and sends valid JSON Web Tokens (JWTs) in the final response to the user.

Creating Cognito User Pool

I am assuming one knows how to create User Pool and its App client. One thing to remember here is if you want custom Auth in your User Pool then you must enable lambda trigger-based custom authentication in app client configuration.

Triggers

In User Pool it is necessary to enable some triggers in order to complete Password-less Authentication.

  • Define Auth Challenge
  • Create Auth Challenge
  • Verify Auth Challenge Response

But before enabling these triggers we must write down some lambdas for their respective functionality.

Custom Authentication Flow

As I mentioned earlier that we will enabling triggers but those triggers need lambdas. Here we will write lambda for every step in custom auth flow.

Here SignUP will not be covered and I am assuming you already have users in your user pool.

Note :

  • I am using Boto3 python library Cognito interaction.
  • Also the Auth Challenge Triggers worked on some per-defined keys which are necessary in order to complete the flow and in case any wrong key or mandatory key is missing then it raise 500 Internal server error.

Define Auth Challenge

Here in Define Auth Challenge every event will receive a session array. If that session array is empty that means Auth have just began and if not then user have answered and it needed to verified that answer is correct or not.

  • In case of empty session we will pass CUSTOM_CHALLENGE in challengeName with other params.
  • If session exists then its result will be preset in challengeResult.
  • We can also allow users some failed attempts to enter correct OTP (I have given 3 here).
  • Additional checks as per your requirement can also be placed here.
def lambda_handler(event, context):
"""Define Auth Challenge Trigger

"""

response = event.get('response')
request = event.get('request')
session = request.get('session')

current_session = len(session) - 1

# # If user is not registered
if request.get('userNotFound') is True:
response.update({
'issueTokens': False,
'failAuthentication': True,+
'msg': "User does not exist"
})

# wrong OTP even After 3 sessions?
elif len(session) >=3 and session[2].get('challengeResult') is False:

response.update({
'issueTokens': False,
'failAuthentication': True
})
# Correct OTP!
elif len(session) > 0 and session[current_session].get('challengeResult') is True:

response.update({
'issueTokens': True,
'failAuthentication': False,
})
# not yet received correct OTP
else:
response.update({
'issueTokens': False,
'failAuthentication': False,
'challengeName': 'CUSTOM_CHALLENGE'
})

return event

Create Auth Challenge

Here challenge is created (OTP in our case) and user is notified about the same on his contact details.

  • privateChallengeParameters this field will be read by Verify Auth Challenge lambda to compare it against the answer.
  • challengeMetadata this field will be persisted across multiple calls to Create Auth Challenge. So that we don’t have to generate a new OTP and notify User for the 2nd and 3rd attempts, instead we can re-use the OTP generated on the 1st attempt
def lambda_handler(event, context):
"""Create Challenge and then Send Notification
"""
response = event.get('response')
request = event.get('request')
session = request.get('session')
if (not session) or len(session) == 0:
secretLoginCode = call_your_create_otp_fn() # create OTP here

# send Notification
contact = request.get('userAttributes').get('email')
# << call_your notify_fn_here()
else:
previousChallenge = session[0]
secretLoginCode = previousChallenge.get('challengeMetadata')
response.update({
'privateChallengeParameters': {'answer': secretLoginCode},
'challengeMetadata': secretLoginCode,
'publicChallengeParameters': {
'answer': secretLoginCode
}
})
return event

Respond To Auth Challenge

Here User will pass his answer to the User Pool and verifyAuthChallenge will verify that.

  • On Correct Answer you will receive tokens
  • On Failed attempt you will receive new session and this new session will be used in next attempt.
def lambda_handler(event, context):
"""Respond To Auth Challenge

"""
challenge = event.get('challenge')
username = event.get('username')
session = event.get('session')
answer = event.get('answer')

response = cognito_client.respond_to_auth_challenge(
ClientId=<<pass_client_id_here>>,
ChallengeName=challenge,
Session=session,
ChallengeResponses={
'USERNAME':username,
'ANSWER': answer
} ,
)
return response

Verify Auth Challenge

This lambda function is responsible to check if the OTP entered by the user is correct or not.

  • challengeAnswer holds OTP entered by user.
  • We can also read the actual generated OTP from the privateChallengeParameter field and compare it to check if the user entered OTP is correct or not.
  • On successful Verification we can update user attributes (here I have updated user email_verified attr. to True).
  • it is mandatory to return answerCorrect so that User Pool can know that user is verified or not.
def lambda_handler(event, context):
"""Verify Auth Challenge

"""

response = event.get('response')
request = event.get('request')
session = request.get('session')

answerCorrect = False

expectedAnswer = request.get('privateChallengeParameters').get('answer')
challengeAnswer = request.get('challengeAnswer')
if challengeAnswer == expectedAnswer:
answerCorrect = True
pool_id = event.get('userPoolId')
userName = event.get('userName')

# Update user Attributes
result = cognito_client.admin_update_user_attributes(
UserPoolId=pool_id,
Username=userName,
UserAttributes=[
{
'Name': 'email_verified',
'Value': 'true'
},
]
)

response.update({
'answerCorrect': answerCorrect
})

return event

SignIn

Here as we are implementing Password Less Auth but cognito still needs password key in auth parameters so we will be sending any random password.

  • Here in SignIn response you will receive session and that session will we used in responseToAuthChallenge.
cognito_client.initiate_auth(
ClientId=client_id,
AuthFlow="CUSTOM_AUTH",
AuthParameters={
'USERNAME': 'my_contact',
'PASSWORD': 'random_password'
}
)

Auto Confirm User

  • If in nay case while signup your user is not confirmed then even if you have answered correct tokens will not be issued.
  • Pool will raise error that User is not confirmed and you cannot confirm user by updating his/her attributes.
  • In this case you can setup an lambda with below code and call it in Pre-Sign UP trigger
def lambda_handler(event, context):
"""Auto Confirm User

"""
print(event, '-------- Start --------')

event.get('response').update({
'autoConfirmUser': True
})

print(event, '-------- end --------')

return event

Summary

In this story I have tried to explain all the key points which will be required to setup Password less authN pipeline. Code of all challenges can be found here. Any suggestion or issues that anyone found in the pipeline I explained or Code I shared are most welcome.

GitHub repo : https://github.com/inforian/cognito-password-less-auth/tree/master

--

--

Neeraj Dhiman
DigiCred
Editor for

Software Engineer by profession. Sports & Bhangra freek. loves to travel and hate study