DEVICE_PASSWORD_VERIFIER challenge response in Amazon Cognito using boto3 and warrant

Question:

I’m using both the boto3 and warrant libraries to try to get a device authenticated to skip multi-factor authentication after it’s been recognized. I’ve got through a user/password auth but can’t seem to figure out the right way to authenticate the device. The following is my code:

from warrant import aws_srp
from warrant.aws_srp import AWSSRP
import boto3
client = boto3.client('cognito-idp')
import datetime

username='xxx'
password='xxx'
client_id='xxx'

aws = AWSSRP(username=username, password=password, pool_id='xxx',
client_id=client_id, client=client)

auth_init = client.initiate_auth(
    AuthFlow='USER_SRP_AUTH',
    AuthParameters={
        'USERNAME': username,
    'SRP_A': aws_srp.long_to_hex(aws.large_a_value),
    },
    ClientId=client_id,
)

cr = aws.process_challenge(auth_init['ChallengeParameters'])
response = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName=auth_init['ChallengeName'],
    ChallengeResponses=cr
)

response = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName='SMS_MFA',
    Session=response['Session'],
    ChallengeResponses={
        'USERNAME': username,
        'SMS_MFA_CODE':'xxx'
    }
)

response_dev = client.confirm_device(
    AccessToken=response['AuthenticationResult']['AccessToken'],
    DeviceKey=response['AuthenticationResult']['NewDeviceMetadata']['DeviceKey'],
DeviceSecretVerifierConfig={ 
    "PasswordVerifier": aws_srp.long_to_hex(aws.large_a_value),
    "Salt": aws_srp.long_to_hex(aws.small_a_value)
}
)

response = client.update_device_status(
    AccessToken=response['AuthenticationResult']['AccessToken'],
    DeviceKey=device,
    DeviceRememberedStatus='remembered'
)

Then on a clean session, do:

device='xxx'

auth_init = client.initiate_auth(
    AuthFlow='USER_SRP_AUTH',
    AuthParameters={
        'USERNAME': username,
    'SRP_A': aws_srp.long_to_hex(aws.large_a_value),
    'DEVICE_KEY':device
    },
    ClientId=client_id,
)

cr = aws.process_challenge(auth_init['ChallengeParameters'])
cr['DEVICE_KEY'] = device
response = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName='DEVICE_SRP_AUTH',
    ChallengeResponses={
        'USERNAME': username,
        'SRP_A': aws_srp.long_to_hex(aws.large_a_value),
        'DEVICE_KEY': device,
        'TIMESTAMP': datetime.datetime.utcnow().strftime( "%a %b %d %H:%M:%S UTC %Y").upper()
    }
)

challenge_params = response['ChallengeParameters']
challenge_params['USER_ID_FOR_SRP'] = challenge_params['USERNAME']
cr2 = aws.process_challenge(challenge_params)

response2 = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName=response['ChallengeName'],
    ChallengeResponses={
        'USERNAME': username,
        'PASSWORD_CLAIM_SIGNATURE': cr2['PASSWORD_CLAIM_SIGNATURE'],
        'PASSWORD_CLAIM_SECRET_BLOCK': response['ChallengeParameters']['SECRET_BLOCK'],
        'DEVICE_KEY': device,
        'TIMESTAMP': datetime.datetime.utcnow().strftime( "%a %b %d %H:%M:%S UTC %Y").upper()
    }
)

Everything seems to work properly until the last respond_to_auth_challenge which results in:
botocore.errorfactory.NotAuthorizedException: An error occurred (NotAuthorizedException) when calling the RespondToAuthChallenge operation: Incorrect username or password.

Should I be using a different User/pass for a DEVICE_PASSWORD_VERIFIER challenge that I haven’t included? The documentation is a bit light, just saying:

DEVICE_PASSWORD_VERIFIER: Similar to PASSWORD_VERIFIER, but for devices only. source

Asked By: jsam

||

Answers:

The reason your solution did not work is because in order for device verifier and salt to work, they have to be calculated based on device_group_key and device_key. Also calculation of a challenge response for device authentication differs from the standard password SRP flow. Here is how it worked out for me:

First, confirm and remember the device (Amazon Cognito Identity SDK for JavaScript source):

from warrant import aws_srp
from warrant.aws_srp import AWSSRP
import boto3
import base64
import os


def generate_hash_device(device_group_key, device_key):
    # source: https://github.com/amazon-archives/amazon-cognito-identity-js/blob/6b87f1a30a998072b4d98facb49dcaf8780d15b0/src/AuthenticationHelper.js#L137

    # random device password, which will be used for DEVICE_SRP_AUTH flow
    device_password = base64.standard_b64encode(os.urandom(40)).decode('utf-8')

    combined_string = '%s%s:%s' % (device_group_key, device_key, device_password)
    combined_string_hash = aws_srp.hash_sha256(combined_string.encode('utf-8'))
    salt = aws_srp.pad_hex(aws_srp.get_random(16))

    x_value = aws_srp.hex_to_long(aws_srp.hex_hash(salt + combined_string_hash))
    g = aws_srp.hex_to_long(aws_srp.g_hex)
    big_n = aws_srp.hex_to_long(aws_srp.n_hex)
    verifier_device_not_padded = pow(g, x_value, big_n)
    verifier = aws_srp.pad_hex(verifier_device_not_padded)

    device_secret_verifier_config = {
        "PasswordVerifier": base64.standard_b64encode(bytearray.fromhex(verifier)).decode('utf-8'),
        "Salt": base64.standard_b64encode(bytearray.fromhex(salt)).decode('utf-8')
    }
    return device_password, device_secret_verifier_config


client = boto3.client('cognito-idp')
username='xxx'
password='xxx'
client_id='xxx'
client_secret='xxx'
pool_id='xxx'

# 1. Login with the password via standard SRP flow
aws = AWSSRP(username=username, password=password, pool_id=pool_id,
             client_id=client_id, client_secret=client_secret, client=client)

auth_init = client.initiate_auth(
    AuthFlow='USER_SRP_AUTH',
    AuthParameters=aws.get_auth_params(),
    ClientId=client_id,
)
cr = aws.process_challenge(auth_init['ChallengeParameters'])
response = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName=auth_init['ChallengeName'],
    ChallengeResponses=cr
)

# 2. Get device_key and device_group_key returned after successful login
device_key = response['AuthenticationResult']['NewDeviceMetadata']['DeviceKey']
device_group_key = response['AuthenticationResult']['NewDeviceMetadata']['DeviceGroupKey']

# 3. Generate random device password, device salt and verifier
device_password, device_secret_verifier_config = generate_hash_device(device_group_key, device_key)
response_dev = client.confirm_device(
    AccessToken=response['AuthenticationResult']['AccessToken'],
    DeviceKey=device_key,
    DeviceSecretVerifierConfig=device_secret_verifier_config,
    DeviceName='some_device_name'
)

# 4. Remember the device
response_dev_upd = client.update_device_status(
    AccessToken=response['AuthenticationResult']['AccessToken'],
    DeviceKey=device_key,
    DeviceRememberedStatus='remembered'
)

Then on a clean session you can login using device credentials (Amazon Cognito Identity SDK for JavaScript source):

import re
import datetime
import base64
import hmac
import hashlib
import boto3

from warrant import aws_srp
from warrant.aws_srp import AWSSRP


class AWSSRPDEV(AWSSRP):
    # source: https://github.com/amazon-archives/amazon-cognito-identity-js/blob/6b87f1a30a998072b4d98facb49dcaf8780d15b0/src/CognitoUser.js#L498

    def __init__(self, username, device_group_key, device_key, device_password,
                 client_id, client, region=None, client_secret=None):
        self.username = username
        self.device_group_key = device_group_key
        self.device_key = device_key
        self.device_password = device_password
        self.client_id = client_id
        self.client_secret = client_secret
        self.client = client or boto3.client('cognito-idp', region_name=region)
        self.big_n = aws_srp.hex_to_long(aws_srp.n_hex)
        self.g = aws_srp.hex_to_long(aws_srp.g_hex)
        self.k = aws_srp.hex_to_long(aws_srp.hex_hash('00' + aws_srp.n_hex + '0' + aws_srp.g_hex))
        self.small_a_value = self.generate_random_small_a()
        self.large_a_value = self.calculate_a()

    def get_auth_params(self):
        auth_params = super(AWSSRPDEV, self).get_auth_params()
        auth_params['DEVICE_KEY'] = self.device_key
        return auth_params

    def get_device_authentication_key(self, device_group_key, device_key, device_password, server_b_value, salt):
        u_value = aws_srp.calculate_u(self.large_a_value, server_b_value)
        if u_value == 0:
            raise ValueError('U cannot be zero.')
        username_password = '%s%s:%s' % (device_group_key, device_key, device_password)
        username_password_hash = aws_srp.hash_sha256(username_password.encode('utf-8'))

        x_value = aws_srp.hex_to_long(aws_srp.hex_hash(aws_srp.pad_hex(salt) + username_password_hash))
        g_mod_pow_xn = pow(self.g, x_value, self.big_n)
        int_value2 = server_b_value - self.k * g_mod_pow_xn
        s_value = pow(int_value2, self.small_a_value + u_value * x_value, self.big_n)
        hkdf = aws_srp.compute_hkdf(bytearray.fromhex(aws_srp.pad_hex(s_value)),
                            bytearray.fromhex(aws_srp.pad_hex(aws_srp.long_to_hex(u_value))))
        return hkdf

    def process_device_challenge(self, challenge_parameters):
        username = challenge_parameters['USERNAME']
        salt_hex = challenge_parameters['SALT']
        srp_b_hex = challenge_parameters['SRP_B']
        secret_block_b64 = challenge_parameters['SECRET_BLOCK']
        # re strips leading zero from a day number (required by AWS Cognito)
        timestamp = re.sub(r" 0(d) ", r" 1 ",
                           datetime.datetime.utcnow().strftime("%a %b %d %H:%M:%S UTC %Y"))
        hkdf = self.get_device_authentication_key(self.device_group_key,
                                                  self.device_key,
                                                  self.device_password,
                                                  aws_srp.hex_to_long(srp_b_hex),
                                                  salt_hex)
        secret_block_bytes = base64.standard_b64decode(secret_block_b64)
        msg = bytearray(self.device_group_key, 'utf-8') + bytearray(self.device_key, 'utf-8') + 
              bytearray(secret_block_bytes) + bytearray(timestamp, 'utf-8')
        hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256)
        signature_string = base64.standard_b64encode(hmac_obj.digest())
        response = {'TIMESTAMP': timestamp,
                    'USERNAME': username,
                    'PASSWORD_CLAIM_SECRET_BLOCK': secret_block_b64,
                    'PASSWORD_CLAIM_SIGNATURE': signature_string.decode('utf-8'),
                    'DEVICE_KEY': self.device_key}
        if self.client_secret is not None:
            response.update({
                "SECRET_HASH":
                    self.get_secret_hash(username, self.client_id, self.client_secret)})
        return response


client = boto3.client('cognito-idp')

username='xxx'
client_id='xxx'
client_secret='xxx'

device_key = 'xxx'
device_group_key = 'xxx'
device_password = 'xxx'

aws_dev = AWSSRPDEV(username=username,
                    device_group_key=device_group_key, device_key=device_key, device_password=device_password,
                    client_id=client_id, client_secret=client_secret, client=client)

# Note that device auth flow doesn't start with client.initiate_auth(),
# but rather with client.respond_to_auth_challenge() straight away
response_auth = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName='DEVICE_SRP_AUTH',
    ChallengeResponses=aws_dev.get_auth_params()
)

cr = aws_dev.process_device_challenge(response_auth['ChallengeParameters'])
response_verifier = client.respond_to_auth_challenge(
    ClientId=client_id,
    ChallengeName='DEVICE_PASSWORD_VERIFIER',
    ChallengeResponses=cr
)

Note that in my case Cognito client does have a client_secret, however the code above should potentially work if it doesn’t.

Answered By: Dmitry Deryabin

To make things a little bit more consice you can use Pycognito python library

So in case you want to pass sms mfs (or software token mfs) challenge:

from pycognito import Cognito
from pycognito.exceptions import SoftwareTokenMFAChallengeException, SMSMFAChallengeException


client_id = 'client_id'
user_pool_id = 'pool_id'

username = 'username'
password = 'password'

user = Cognito(user_pool_id,client_id, username=username)
try:
    user.authenticate(password)
except SoftwareTokenMFAChallengeException:
    code = input("Enter the code from your authenticator app: ")
    user.respond_to_software_token_mfa_challenge(code)
except SMSMFAChallengeException:
    code = input("Enter the SMS code: ")
    user.respond_to_sms_mfa_challenge(code)
print(f"My access token: {user.access_token}")
Answered By: The Hog