implementing USER_SRP_AUTH with python boto3 for AWS Cognito
Asked Answered
C

3

23

Amazon provides iOS, Android, and Javascript Cognito SDKs that offer a high-level authenticate-user operation.

For example, see Use Case 4 here:

https://github.com/aws/amazon-cognito-identity-js

However, if you are using python/boto3, all you get are a pair of primitives: cognito.initiate_auth and cognito.respond_to_auth_challenge.

I am trying to use these primitives along with the pysrp lib authenticate with the USER_SRP_AUTH flow, but what I have is not working.

It always fails with "An error occurred (NotAuthorizedException) when calling the RespondToAuthChallenge operation: Incorrect username or password." (The username/password pair work fine with the JS SDK.)

My suspicion is I'm constructing the challenge response wrong (step 3), and/or passing Congito hex strings when it wants base64 or vice versa.

Has anyone gotten this working? Anyone see what I'm doing wrong?

I am trying to copy the behavior of the authenticateUser call found in the Javascript SDK:

https://github.com/aws/amazon-cognito-identity-js/blob/master/src/CognitoUser.js#L138

but I'm doing something wrong and can't figure out what.

#!/usr/bin/env python
import base64
import binascii
import boto3
import datetime as dt
import hashlib
import hmac

# http://pythonhosted.org/srp/
# https://github.com/cocagne/pysrp
import srp

bytes_to_hex = lambda x: "".join("{:02x}".format(ord(c)) for c in x)

cognito = boto3.client('cognito-idp', region_name="us-east-1")

username = "[email protected]"
password = "123456"

user_pool_id = u"us-east-1_XXXXXXXXX"
client_id = u"XXXXXXXXXXXXXXXXXXXXXXXXXX"

# Step 1:
# Use SRP lib to construct a SRP_A value.

srp_user = srp.User(username, password)
_, srp_a_bytes = srp_user.start_authentication()

srp_a_hex = bytes_to_hex(srp_a_bytes)

# Step 2:
# Submit USERNAME & SRP_A to Cognito, get challenge.

response = cognito.initiate_auth(
    AuthFlow='USER_SRP_AUTH',
    AuthParameters={ 'USERNAME': username, 'SRP_A': srp_a_hex },
    ClientId=client_id,
    ClientMetadata={ 'UserPoolId': user_pool_id })

# Step 3:
# Use challenge parameters from Cognito to construct 
# challenge response.

salt_hex         = response['ChallengeParameters']['SALT']
srp_b_hex        = response['ChallengeParameters']['SRP_B']
secret_block_b64 = response['ChallengeParameters']['SECRET_BLOCK']

secret_block_bytes = base64.standard_b64decode(secret_block_b64)
secret_block_hex = bytes_to_hex(secret_block_bytes)

salt_bytes = binascii.unhexlify(salt_hex)
srp_b_bytes = binascii.unhexlify(srp_b_hex)

process_challenge_bytes = srp_user.process_challenge(salt_bytes,                          
                                                     srp_b_bytes)
    
timestamp = unicode(dt.datetime.utcnow().strftime("%a %b %d %H:%m:%S +0000 %Y"))

hmac_obj = hmac.new(process_challenge_bytes, digestmod=hashlib.sha256)
hmac_obj.update(user_pool_id.split('_')[1].encode('utf-8'))
hmac_obj.update(username.encode('utf-8'))
hmac_obj.update(secret_block_bytes)
hmac_obj.update(timestamp.encode('utf-8'))

challenge_responses = {
    "TIMESTAMP": timestamp.encode('utf-8'),
    "USERNAME": username.encode('utf-8'),
    "PASSWORD_CLAIM_SECRET_BLOCK": secret_block_hex,
    "PASSWORD_CLAIM_SIGNATURE": hmac_obj.hexdigest()
}

# Step 4:
# Submit challenge response to Cognito.

response = cognito.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName='PASSWORD_VERIFIER',
    ChallengeResponses=challenge_responses)
Cacie answered 7/1, 2017 at 20:29 Comment(3)
Did you ever get this working? I'm working on the same thing in my project.Danforth
No, I banged on it a bit further, but no luck so far. As a work around, I have set up a custom-auth lambda (DefineAuthChallenge) that just always auths a user in: exports.handler = function(event, context) {event.response.issueTokens = true; event.response.failAuthentication = false; context.done(null, event);}. This is all I need for now since I'm just building a prototype. But I'm counting on being able to get this working eventually.Cacie
man2xxl, checkout armicron's answer below.Cacie
D
24

There are many errors in your implementation. For example:

  1. pysrp uses SHA1 algorithm by default. It should be set to SHA256.
  2. _ng_const length should be 3072 bits and it should be copied from amazon-cognito-identity-js
  3. There is no hkdf function in pysrp.
  4. The response should contain secret_block_b64, not secret_block_hex.
  5. Wrong timestamp format. %H:%m:%S means "hour:month:second" and +0000 should be replaced by UTC.

Has anyone gotten this working?

Yes. It's implemented in the warrant.aws_srp module. https://github.com/capless/warrant/blob/master/warrant/aws_srp.py

from warrant.aws_srp import AWSSRP


USERNAME='xxx'
PASSWORD='yyy'
POOL_ID='us-east-1_zzzzz'
CLIENT_ID = '12xxxxxxxxxxxxxxxxxxxxxxx'

aws = AWSSRP(username=USERNAME, password=PASSWORD, pool_id=POOL_ID,
             client_id=CLIENT_ID)
tokens = aws.authenticate_user()
id_token = tokens['AuthenticationResult']['IdToken']
refresh_token = tokens['AuthenticationResult']['RefreshToken']
access_token = tokens['AuthenticationResult']['AccessToken']
token_type = tokens['AuthenticationResult']['TokenType']

authenticate_user method supports only PASSWORD_VERIFIER challenge. If you want to respond to other challenges, just look into the authenticate_user and boto3 documentation.

Discrown answered 27/3, 2017 at 12:28 Comment(2)
Unfortunately, warrant is no longer maintained and its most recent version depends on a broken version of pycryptodome.Eductive
@CameronHudson the current warrant version (0.6.1) appears to work fine in the example above. The current pycryptodome is newer than your comment so maybe it's OK now?Darcidarcia
S
2

Unfortunately it's a hard problem since you don't get any hints from the service with regards to the computations (it mainly says not authorized as you mentioned).

We are working on improving the developer experience when users are trying to implement SRP on their own in languages where we don't have an SDK. Also, we are trying to add more SDKs.

As daunting as it sounds, what I would suggest is to take the Javascript or the Android SDK, fix the inputs (SRP_A, SRP_B, TIMESTAMP) and add console.log statements at various points in the implementation to make sure your computations are similar. Then you would run these computations in your implementation and make sure you are getting the same. As you have suggested, the password claim signature needs to be passed as a base64 encoded string to the service so that might be one of the issues.

Some of the issues I encountered while implementing this was related to BigInteger library differences (the way they do byte padding and transform negative numbers to byte arrays and inversely).

Shortening answered 9/1, 2017 at 18:50 Comment(2)
Thanks Ionut. I was hoping someone would see that I got some step obviously wrong, and I could avoid the painful process you are proposing, but I guess I'm stuck. Thanks for the tip on PASSWORD_CLAIM_SIGNATURE. I'm presuming you mean base64 encode the raw bytes returned from the HMAC operation, right? (And not say, a base64 of the hexstring of those bytes?)Cacie
Yes that is what I meant, base64 encode the raw bytes returned from the hmac operation.Shortening
C
1

I have a similar alternative to @armicron using pycognito library.

import boto3
from pycognito import AWSSRP

session = boto3.Session(profile_name="aws-profile")
cognito = session.client('cognito-idp')

cognito_user_pool_id = "xx-xxxx-x_xxxxxxxxx"
cognito_user_pool_client_id = "xxxxxxxxxxxxxxxxxxxxxxxxxx"
username = "xxxxxxxxxxxxx"
password = "xxxxxxxxxxxxx"

aws_srp = AWSSRP(
    username=username,
    password=password,
    pool_id=cognito_user_pool_id,
    client_id=cognito_user_pool_client_id,
    client=cognito
)

# Initiate Auth
print("\nInitiate Auth Started")
auth_params = aws_srp.get_auth_params()
resp = cognito.initiate_auth(
    AuthFlow='USER_SRP_AUTH',
    AuthParameters=auth_params,
    ClientId=cognito_user_pool_client_id
)
print(resp)

# Respond to PASSWORD_VERIFIER challenge
print("\nPASSWORD_VERIFIER Started")
assert resp["ChallengeName"] == "PASSWORD_VERIFIER"
challenge_response = aws_srp.process_challenge(resp["ChallengeParameters"], auth_params)
resp = cognito.respond_to_auth_challenge(
    ClientId=cognito_user_pool_client_id,
    ChallengeName="PASSWORD_VERIFIER",
    ChallengeResponses=challenge_response
)
print(resp)

That can be useful when you want to test the CUSTOM_AUTH flow using SRP passwords.

Cristacristabel answered 7/9, 2023 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.