Passwordless authentication flow using Cognito & API Gateway & Lambda (Python)

2.4k views Asked by At

I've been trying to implement passwordless authentication using AWS Cognito & API Gateway & Lambda (Python)

I have followed these articles: https://medium.com/digicred/password-less-authentication-in-cognito-cafa016d4db7 https://medium.com/@pjatocheseminario/passwordless-api-using-cognito-and-serverless-framework-7fa952191352

I have configured Cognito (to accept CUSTOM_AUTH), added the Lambdas, and created the API endpoints:

/sign-up 
/initiate-auth (aka initiate login)
/respond-to-auth-challenge (aka (verify login)

When calling initiateAuth I receive the following response: An error occurred (NotAuthorizedException) when calling the InitiateAuth operation: Incorrect username or password."

I'm using CUSTOM_AUTH which doesn't require password, and the user name is definitely correct because it actually initiates the authentication flow and I receive a code, however because boto3 doesn't respond with a session I can't continue the authentication.

This is how I call Cognito:

res = cognito.initiate_auth(
        ClientId=client_id,
        AuthFlow="CUSTOM_AUTH",
        AuthParameters={
            "USERNAME": email,
            "PASSWORD": random_password  
            }
        )

It's probably something small I'm missing but I can't figure out what.

2

There are 2 answers

0
Frank Lin On BEST ANSWER

I was facing the same error, and I think that the error message is misleading. When you did not respond correctly in Create-Auth-Challenge lambda, you will get this error. So make sure everything is right in your lambda.

1
Takuto Lehr On

Your client code looks OK, mine has ClientId param in it but if your code is not raising an exception then it should be fine. Unless you had Generate client secret option checked when you created your app client. If that is the case then you have to pass in SECRET_HASH in AuthParameters like the following:

import hmac
import hashlib
import base64

def get_secret_hash(email, client_id, client_secret):                                                                   
     """                                                                                                                 
     A keyed-hash message authentication code (HMAC) calculated using                                                    
     the secret key of a user pool client and username plus the client                                                   
     ID in the message.                                                                                                  
     """                                                                                                                 
     message = email + client_id                                                                                         
     client_secret = str.encode(client_secret)                                                                           
     dig = hmac.new(client_secret, msg=message.encode('UTF-8'), digestmod=hashlib.sha256).digest()                       
     return base64.b64encode(dig).decode()
                                                                                                                 
client.admin_initiate_auth(                                                                                  
    UserPoolId=COGNITO_USER_POOL_ID,                                                                                    
    ClientId=CLIENT_ID,                                                                                                 
    AuthFlow='CUSTOM_AUTH',                                                                                             
    AuthParameters={                                                                                                    
        'USERNAME': email,                                                                                              
        'SECRET_HASH': get_secret_hash(email, CLIENT_ID, CLIENT_SECRET) # Omit if secret key option is disabled.                                                          
    },
) 

Next, double check the following:

Under App clients > * > Auth Flows Configuration, is ALLOW_CUSTOM_AUTH option enabled for your client?

Under App integration > App client settings > * > Enabled Identity Providers, is your user pool selected?

If you have Cognito setup correctly and your code still doesn't work then it is probably the lambda code. You probably know this but for password-less custom auth you need to use 3 lambda triggers: Define Auth Challenge, Create Auth Challenge, and Verify Auth Challenge.

Custom auth lambdas events are triggered in the following order:

  1. DefineAuthChallenge_Authentication:
    • Technically, issueTokens can be set to True here to return tokens without going through the rest of the steps.
def lambda_handler(event, context):
    if event['triggerSource'] == 'DefineAuthChallenge_Authentication':
        event['response']['challengeName'] = 'CUSTOM_CHALLENGE'
        event['response']['issueTokens'] = False
        event['response']['failAuthentication'] = False

        if event['request']['session']:  # Needed for step 4.
            # If all of the challenges are answered, issue tokens.
            event['response']['issueTokens'] = all(
                answered_challenge['challengeResult'] for answered_challenge in event['request']['session'])
    return event
  1. CreateAuthChallenge_Authentication:
def lambda_handler(event, context):
    if event['triggerSource'] == 'CreateAuthChallenge_Authentication':
        if event['request']['challengeName'] == 'CUSTOM_CHALLENGE':
            event['response']['privateChallengeParameters'] = {}
            event['response']['privateChallengeParameters']['answer'] = 'YOUR CHALLENGE ANSWER HERE'
            event['response']['challengeMetadata'] = 'AUTHENTICATE_AS_CHALLENGE'
    return event

Then your client must respond to the challenge:

client.respond_to_auth_challenge(                                                                            
    ClientId=CLIENT_ID,                                                                                                 
    ChallengeName='CUSTOM_CHALLENGE',                                                                                   
    Session=session,                                                                                                    
    ChallengeResponses={                                                                                                
        'USERNAME': email,                                                                                              
        'ANSWER': 'Extra Protection!',                                                                                  
        'SECRET_HASH': get_secret_hash(email, CLIENT_ID, CLIENT_SECRET)  # Omit if secret key option is disabled.                                                 
    }                                                                                                                   
)    
  1. VerifyAuthChallengeResponse_Authentication:
def lambda_handler(event, context):
    if event['triggerSource'] == 'VerifyAuthChallengeResponse_Authentication':
        if event['request']['challengeAnswer'] == event['request']['privateChallengeParameters']['answer']:
            event['response']['answerCorrect'] = True
    return event
  1. DefineAuthChallenge_Authentication:
    • Set event['response']['issueTokens'] to True to return tokens (code shown in step 1), or issue another challenge to keep repeating steps 1-3.

Finally, make sure that if case-insensitivity option is enabled for your user pool too. Also, I can't exactly recall if CUSTOM_AUTH flow worked if the user is in FORCE_CHANGE_PASSWORD status. If the user is in that state, then try settings a permanent password with the sdk to set the status to CONFIRMED.