Part 1, Serverless

Graphicaldot (Saurav verma)
8 min readMay 22, 2019

--

AWS cognito with Python.

Introduction

The login page is the fist thing that most web application users encounter. Account creation is the gateway through which all new application users pass through before they can use a web application. This means that authentication (account creation, login and user data management) is a critical component for most web applications.

Cognito is designed for a variety of application use cases. Cognito can be used for client side authentication of mobile devices, client side web applications (using JavaScript) and for server side authentication (the application that is discussed in this article). The Cognito infrastructure can even provide web pages for the various authentication tasks.

Cognito User Pools

Cognito can support one more more “user pools”. Each “pool” contains the login and user information for a group of users. Production and test user pools can be created so that application testing does not impact the Cognito production user information.

Cognito also provides a user interface that allows management of users within a particular pool. This user interface is available via the AWS console login, which can be protected with two factor authentication.

Cognito Authentication Support

The AWS Cognito service provides support for a wide range of authentication features, For example, Cognito can support two factor authentication for high security applications and OAuth, which allows an application to authenticate using an OAuth provider like Google, Facebook or Twitter.

Cognito supports the steps needed to securely create an application account. This includes sending a temporary password to the user’s email and temporary authentication using this password, which allows the user to create a permanent password.

Cognito also supports password reset for an existing account for the case where a user has forgotten their password. Cognito will email the user a code, which can be used to create a new password or can also send a code to the registered mobile number. There are two ways, in which the codes can be sent. You can use the default cognito messaging or can integrate with SNS/SES.

The diagram below illustrates the web page flow for new accounts and forgotten passwords. In this demonstration application, the Spring code supports the control flow via Spring controller objects.

Lets me first walk you to the steps needed to create a user pool on AWS cognito.

  1. Go to the Amazon Cognito console. You might be prompted for your AWS credentials.
  2. Choose Manage your User Pools.
  3. In the top-right corner of the page, choose Create a User Pool.
  4. Provide a name for your user pool, and choose Review Defaults to save the name.
  5. On the Attributes page, choose Email address or phone number and Allow email addresses.
  1. At the bottom of the page, choose Next Step to save the attribute.
  2. On the navigation bar on the left-side of the page, choose Review.
  3. On the bottom of the Review page, choose Create pool.

For this tutorial, I have chosen the first option Username, which means that users can choose their unique usernames and sign up with them.

Note down you pool-id

Next is to create app-client-id.

  1. On the navigation bar on the left-side of the page, choose App clients under General settings.
  2. Choose Add an app client.
  3. Give your app a name.
  4. Check generate client secret, it will be required in our lambda functions and check server-based authentication (ADMIN_NO_SRP_AUTH).
  1. Choose Create app client.
  2. Note the App client ID and APP Client Secret.

Now, Our AWS cognito user pool is created and we now need to create lambda functions to interact with it.

Lambda function: Signup

When a user needs to sign in, he/she has to decide on their primary email id and a password of atleast 8 characters long. The lambda function which will handle new user registration.

import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import json
USER_POOL_ID = ''
CLIENT_ID = ''
CLIENT_SECRET = ''
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(str(CLIENT_SECRET).encode('utf-8'),
msg = str(msg).encode('utf-8'), digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def lambda_handler(event, context): for field in ["username", "email", "password", "name"]:
if not event.get(field):
return {"error": False, "success": True, 'message': f"{field} is not present", "data": None}
username = event['username']
email = event["email"]
password = event['password']
name = event["name"]
client = boto3.client('cognito-idp') try:
resp = client.sign_up(
ClientId=CLIENT_ID,
SecretHash=get_secret_hash(username),
Username=username,
Password=password,
UserAttributes=[
{
'Name': "name",
'Value': name
},
{
'Name': "email",
'Value': email
}
],
ValidationData=[
{
'Name': "email",
'Value': email
},
{
'Name': "custom:username",
'Value': username
}
])


except client.exceptions.UsernameExistsException as e:
return {"error": False,
"success": True,
"message": "This username already exists",
"data": None}
except client.exceptions.InvalidPasswordException as e:

return {"error": False,
"success": True,
"message": "Password should have Caps,\
Special chars, Numbers",
"data": None}
except client.exceptions.UserLambdaValidationException as e:
return {"error": False,
"success": True,
"message": "Email already exists",
"data": None}

except Exception as e:
return {"error": False,
"success": True,
"message": str(e),
"data": None}

return {"error": False,
"success": True,
"message": "Please confirm your signup, \
check Email for validation code",
"data": None}

*Note: this exception (UserLambdaValidationException) will be raised be another pre signup lambda function which will check if the same email exists in cognito or not. I will exlain this in another post.

Lambda : Confirm Sign up

If a user is a new registration, She/He will be asked to verify her/his email id through a verification code sent on her/his registered email id.

import json
import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import uuid
USER_POOL_ID = '<your user pool id>'
CLIENT_ID = '<your client id>'
CLIENT_SECRET ='<your client secret>'
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(str(CLIENT_SECRET).encode('utf-8'),
msg = str(msg).encode('utf-8'), digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def lambda_handler(event, context):
client = boto3.client('cognito-idp')
try:
username = event['username']
password = event['password']
code = event['code']
response = client.confirm_sign_up(
ClientId=CLIENT_ID,
SecretHash=get_secret_hash(username),
Username=username,
ConfirmationCode=code,
ForceAliasCreation=False,
)
except client.exceptions.UserNotFoundException:
#return {"error": True, "success": False, "message": "Username doesnt exists"}
return event
except client.exceptions.CodeMismatchException:
return {"error": True, "success": False, "message": "Invalid Verification code"}

except client.exceptions.NotAuthorizedException:
return {"error": True, "success": False, "message": "User is already confirmed"}

except Exception as e:
return {"error": True, "success": False, "message": f"Unknown error {e.__str__()} "}

return event

Lambda : Resend Verification code

The verification is valid only for 24 hours, Lets say the user missed that window to confirm her/his registration, then he can request the verification code again from cognito.

import json
import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import uuid
USER_POOL_ID = '<your user pool id>'
CLIENT_ID = '<your client id>'
CLIENT_SECRET ='<your client secret>'
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(str(CLIENT_SECRET).encode('utf-8'),
msg = str(msg).encode('utf-8'),
digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def lambda_handler(event, context):
client = boto3.client('cognito-idp')
try:
username = event['username']
response = client.resend_confirmation_code(
ClientId=CLIENT_ID,
SecretHash=get_secret_hash(username),
Username=username,
)
except client.exceptions.UserNotFoundException:
return {"error": True, "success": False, "message": "Username doesnt exists"}

except client.exceptions.InvalidParameterException:
return {"error": True, "success": False, "message": "User is already confirmed"}

except Exception as e:
return {"error": True, "success": False, "message": f"Unknown error {e.__str__()} "}

return {"error": False, "success": True}

Lambda: Forgot Password

Lambda function to send an email to the user , in case he forgets her/his password. Invocation of this lambda function will send a verification code to the email/phonenumber.

import json
import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import uuid
USER_POOL_ID = '<your user pool id>'
CLIENT_ID = '<your client id>'
CLIENT_SECRET ='<your client secret>'
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(
str(CLIENT_SECRET).encode('utf-8'),
msg = str(msg).encode('utf-8'),
digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def lambda_handler(event, context):
client = boto3.client('cognito-idp')
try:
username = event['username']
response = client.forgot_password(
ClientId=CLIENT_ID,
SecretHash=get_secret_hash(username),
Username=username,

)
except client.exceptions.UserNotFoundException:
return {"error": True,
"data": None,
"success": False,
"message": "Username doesnt exists"}

except client.exceptions.InvalidParameterException:
return {"error": True,
"success": False,
"data": None,
"message": f"User <{username}> is not confirmed yet"}

except client.exceptions.CodeMismatchException:
return {"error": True,
"success": False,
"data": None,
"message": "Invalid Verification code"}

except client.exceptions.NotAuthorizedException:
return {"error": True,
"success": False,
"data": None,
"message": "User is already confirmed"}

except Exception as e:
return {"error": True,
"success": False,
"data": None,
"message": f"Uknown error {e.__str__()} "}

return {
"error": False,
"success": True,
"message": f"Please check your Registered email id for validation code",
"data": None}

Lambda: Confirm forgot password

After the user receives a code on her/his email or phone number, a different lambda functions needs invocation, to check authenticity of the validation code. If the verification code is correct, User password will be replaced with the new password.

import json
import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import uuid
USER_POOL_ID = '<your user pool id>'
CLIENT_ID = '<your client id>'
CLIENT_SECRET ='<your client secret>'
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(str(CLIENT_SECRET).encode('utf-8'),
msg = str(msg).encode('utf-8'), digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def lambda_handler(event, context):
client = boto3.client('cognito-idp')
try:
username = event['username']
password = event['password']
code = event['code']
client.confirm_forgot_password(
ClientId=CLIENT_ID,
SecretHash=get_secret_hash(username),
Username=username,
ConfirmationCode=code,
Password=password,
)
except client.exceptions.UserNotFoundException as e:
return {"error": True,
"success": False,
"data": None,
"message": "Username doesnt exists"}
except client.exceptions.CodeMismatchException as e:
return {"error": True,
"success": False,
"data": None,
"message": "Invalid Verification code"}

except client.exceptions.NotAuthorizedException as e:
return {"error": True,
"success": False,
"data": None,
"message": "User is already confirmed"}

except Exception as e:
return {"error": True,
"success": False,
"data": None,
"message": f"Unknown error {e.__str__()} "}

return {"error": False,
"success": True,
"message": f"Password has been changed successfully",
"data": None}

You can check which all users are currently signed up with your app, and which all users are in confirmed or unconfirmed state on cognito console.

The last bit is to provide login for the users who have successfully completed their registration process.

import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import json
USER_POOL_ID = ''
CLIENT_ID = ''
CLIENT_SECRET = ''
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(str(CLIENT_SECRET).encode('utf-8'),
msg = str(msg).encode('utf-8'), digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def initiate_auth(client, username, password):
secret_hash = get_secret_hash(username)
try:
resp = client.admin_initiate_auth(
UserPoolId=USER_POOL_ID,
ClientId=CLIENT_ID,
AuthFlow='ADMIN_NO_SRP_AUTH',
AuthParameters={
'USERNAME': username,
'SECRET_HASH': secret_hash,
'PASSWORD': password,
},
ClientMetadata={
'username': username,
'password': password,
}) except client.exceptions.NotAuthorizedException:
return None, "The username or password is incorrect"
except client.exceptions.UserNotConfirmedException:
return None, "User is not confirmed"
except Exception as e:
return None, e.__str__()
return resp, None
def lambda_handler(event, context):
client = boto3.client('cognito-idp')
for field in ["username", "password"]:
if event.get(field) is None:
return {"error": True,
"success": False,
"message": f"{field} is required",
"data": None}
resp, msg = initiate_auth(client, username, password)
if msg != None:
return {'message': msg,
"error": True, "success": False, "data": None}
if resp.get("AuthenticationResult"):
return {'message': "success",
"error": False,
"success": True,
"data": {
"id_token": resp["AuthenticationResult"]["IdToken"],
"refresh_token": resp["AuthenticationResult"]["RefreshToken"],
"access_token": resp["AuthenticationResult"]["AccessToken"],
"expires_in": resp["AuthenticationResult"]["ExpiresIn"]
"token_type": resp["AuthenticationResult"]["TokenType"]
}}
else: #this code block is relevant only when MFA is enabled
return {"error": True,
"success": False,
"data": None, "message": None}

In the next part, we will explore linking these lambda functions with API Gateway to access signup functionalities and also the use of access token to secure our other application API’s. Cheers!

My Eth Address: 0x01445B7d9D63381D0e7A6d8556F62A24197BeA1F

My Bitcoin Address: bc1qhdzydl7kwjk8reznvj3yd6s0cg7e7r2sasgfxc

--

--

Graphicaldot (Saurav verma)

My mission is to protect your data and privacy on Web3. Work( @0xPolygon , privateInput=position) - Yes Work( @Biconomy , privateInput=position) - Yes