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
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.