Closed dcolclazier closed 3 years ago
Any update on this?
@dcolclazier In your step 3 above, how do you set creds
? Also, I do not see an overloaded constructor for ConfirmDeviceAsync
to pass creds
and NewDeviceInfo
. The ConfirmDeviceRequest
class does not have creds
nor NewDeviceInfo
.
Hey Thomas,
Thanks for looking into this! Our "creds" object is as follows:
var userDetailsObj = await user.GetUserDetailsAsync();
var userCreds = user.GetCognitoAWSCredentials(_settings.IdentityPoolId, Amazon.RegionEndpoint.USEast1).GetCredentials();
var creds = new Credentials
{
Region = Amazon.RegionEndpoint.USEast1.ToString(),
Username = userName,
UserEmail = userDetailsObj.UserAttributes.First(a => a.Name == "email").Value,
ExternalId = userDetailsObj.UserAttributes.First(a => a.Name == "custom:systm_identity").Value,
IssuedOn = DateTime.Now,
UserPoolId = _settings.UserPoolId,
IdentityPoolId = _settings.IdentityPoolId,
ClientId = _settings.ClientId,
ExpireTimeString = DateTime.UtcNow.AddSeconds(authResponse.AuthenticationResult.ExpiresIn).ToString(),
TemporaryIdentityPoolToken = userCreds.Token,
TemporaryIAMAccessKey = userCreds.AccessKey,
TemporaryIAMSecretKey = userCreds.SecretKey,
UserPoolIdToken = authResponse.AuthenticationResult.IdToken,
UserPoolRefreshToken = authResponse.AuthenticationResult.RefreshToken,
UserPoolAccessToken = authResponse.AuthenticationResult.AccessToken,
DeviceKey = user.Device?.DeviceKey ?? string.Empty
};
In addition, here is the ConfirmDeviceAsync logic
public async Task<ConfirmDeviceResponse> ConfirmDeviceAsync(Credentials credentials, NewDeviceInfo newDeviceInfo)
{
try
{
var authenticationHelper = new AuthenticationHelper();
_settings.DeviceVerifier = authenticationHelper.GenerateHashDevice(newDeviceInfo.GroupKey, newDeviceInfo.Key);
_settings.Save();
_settings.Reload();
var confirmDeviceRequest = new ConfirmDeviceRequest
{
DeviceKey = newDeviceInfo.Key,
AccessToken = credentials.UserPoolAccessToken,
DeviceName = newDeviceInfo.Name,
DeviceSecretVerifierConfig = new DeviceSecretVerifierConfigType()
{
PasswordVerifier = Convert.ToBase64String(authenticationHelper.GetVerifierBytes()),
Salt = Convert.ToBase64String(authenticationHelper.GetSaltBytes())
}
};
var response = await _cognitoClient.ConfirmDeviceAsync(confirmDeviceRequest);
return response;
}
catch (Exception error)
{
string errorMessage = " - ERROR in Confirm Device - " + Environment.NewLine;
if (error.Data.Contains("CogntioFail"))
{
error.Data["CogntioFail"] += errorMessage;
}
else
{
error.Data.Add("CogntioFail", errorMessage);
}
throw error;
}
}
Let me know if you need our implementation of AuthenticationHelper - I can confirm we can get a successful response from
var response = await _cognitoClient.ConfirmDeviceAsync(confirmDeviceRequest);
I should mention - the original issue is caused by how the device key is added to the ChallengeResponse - see the following line of code in CognitoUserAuthentication.cs
if (challengeResponsesValid && deviceKeyValid)
{
challengeRequest.ChallengeResponses.Add(CognitoConstants.ChlgParamDeviceKey, Device.DeviceKey);
}
should be
if (challengeResponsesValid && deviceKeyValid)
{
challengeRequest.ChallengeResponses[CognitoConstants.ChlgParamDeviceKey] = Device.DeviceKey;
}
Also, if a device key is provided during StartWithSrpAuthAsync, an additional "DEVICE_SRP_AUTH" challenge request comes back from Cognito after the initial (successful) challenge request, which isn't being handled by your SDK.
I've made a bit of progress on this; maybe you can help me out:
The authFlow for StartWithSrpAuthAsync if a Device Key is added to the user is as follows:
SRP_AUTH PASSWORD_VERIFIER DEVICE_SRP_AUTH DEVICE_PASSWORD_VERIFIER
I have added the necessary functionality to make it past the third flow, but then hit a snag on DEVICE_PASSWORD_VERIFIER (incessant Invalid User or Password errors).
First, salt & v generation, valid when confirming the new device prior to this auth flow (inside Amazon.Extensions.CognitoAuthentication.AuthenticationHelper):
public static Tuple<string, string, string> GenerateDeviceVerifier(string deviceGroupKey, string deviceKey)
{
var randomPass = Convert.ToBase64String(RandomBytes(40));
byte[] userIdContent = CognitoAuthHelper.CombineBytes(new byte[][] {
Encoding.UTF8.GetBytes(deviceGroupKey),
Encoding.UTF8.GetBytes(deviceKey),
Encoding.UTF8.GetBytes(":"),
Encoding.UTF8.GetBytes(randomPass)
});
byte[] userIdHash = CognitoAuthHelper.Sha256.ComputeHash(userIdContent);
var salt = BigInteger.Parse(BitConverter.ToString(RandomBytes(16)).Replace("-", string.Empty), NumberStyles.HexNumber);
var saltBytes = salt.ToBigEndianByteArray();
byte[] xBytes = CognitoAuthHelper.CombineBytes(new byte[][] { saltBytes, userIdHash });
byte[] xDigest = CognitoAuthHelper.Sha256.ComputeHash(xBytes);
BigInteger x = BigIntegerExtensions.FromUnsignedBigEndian(xDigest);
var v = BigInteger.ModPow(g, x, N);
var vBytes = v.ToBigEndianByteArray();
return Tuple.Create(randomPass, Convert.ToBase64String(vBytes), Convert.ToBase64String(saltBytes));
}
private static byte[] RandomBytes(int size)
{
var bytes = new byte[size];
new Random().NextBytes(bytes);
return bytes;
}
Then, CognitoUserAuthentication.StartWithSrpAuthAsync():
/// <summary>
/// Initiates the asynchronous SRP authentication flow
/// </summary>
/// <param name="srpRequest">InitiateSrpAuthRequest object containing the necessary parameters to
/// create an InitiateAuthAsync API call for SRP authentication</param>
/// <returns>Returns the AuthFlowResponse object that can be used to respond to the next challenge,
/// if one exists</returns>
public async Task<AuthFlowResponse> StartWithSrpAuthAsync(InitiateSrpAuthRequest srpRequest)
{
#region User SRP Auth
if (srpRequest == null || string.IsNullOrEmpty(srpRequest.Password))
{
throw new ArgumentNullException("Password required for authentication.", "srpRequest");
}
Tuple<BigInteger, BigInteger> tupleAa = AuthenticationHelper.CreateAaTuple();
InitiateAuthRequest initiateRequest = CreateSrpAuthRequest(tupleAa);
InitiateAuthResponse initiateResponse = await Provider.InitiateAuthAsync(initiateRequest).ConfigureAwait(false);
UpdateUsernameAndSecretHash(initiateResponse.ChallengeParameters);
#endregion
#region User Password Verifier
RespondToAuthChallengeRequest challengeRequest =
CreateSrpPasswordVerifierAuthRequest(initiateResponse, srpRequest.Password, tupleAa);
bool challengeResponsesValid = challengeRequest != null && challengeRequest.ChallengeResponses != null;
bool deviceKeyValid = Device != null && !string.IsNullOrEmpty(Device.DeviceKey);
if (challengeResponsesValid && deviceKeyValid)
{
challengeRequest.ChallengeResponses[CognitoConstants.ChlgParamDeviceKey] = Device.DeviceKey;
}
var verifierResponse = await Provider.RespondToAuthChallengeAsync(challengeRequest).ConfigureAwait(false);
#endregion
//this response comes back with a "DEVICE_SRP_AUTH" challenge, because there is a Device attached to the user.
//DEVICE_SRP_AUTH requires USERNAME, DEVICE_KEY, SRP_A (and SECRET_HASH).
//DEVICE_PASSWORD_VERIFIER requires PASSWORD_CLAIM_SIGNATURE , PASSWORD_CLAIM_SECRET_BLOCK , TIMESTAMP , USERNAME, and DEVICE_KEY .
#region Device SRP Auth
if(verifierResponse.AuthenticationResult == null)
{
if (verifierResponse == null || string.IsNullOrEmpty(srpRequest.DeviceGroupKey) || string.IsNullOrEmpty(srpRequest.DevicePass))
{
throw new ArgumentNullException("Device Group Key and Device Pass required for authentication.", "srpRequest");
}
var deviceAuthRequest = CreateDeviceSrpAuthRequest(tupleAa);
var deviceAuthResponse = await RespondToDeviceSrpAuthAsync(deviceAuthRequest).ConfigureAwait(false);
UpdateUsernameAndSecretHash(deviceAuthResponse.ChallengeParameters);
#region Device Password Verifier
var devicePasswordChallengeRequest = CreateDevicePasswordVerifierAuthRequest(deviceAuthResponse, srpRequest.DeviceGroupKey, srpRequest.DevicePass, tupleAa);
verifierResponse = await Provider.RespondToAuthChallengeAsync(devicePasswordChallengeRequest).ConfigureAwait(false);
#endregion
}
#endregion
UpdateSessionIfAuthenticationComplete(verifierResponse.ChallengeName, verifierResponse.AuthenticationResult);
return new AuthFlowResponse(verifierResponse.Session,
verifierResponse.AuthenticationResult,
verifierResponse.ChallengeName,
verifierResponse.ChallengeParameters,
new Dictionary<string, string>(verifierResponse.ResponseMetadata.Metadata));
}
During this, we create the DevicePasswordVerifierRequest:
/// <summary>
/// Internal method which responds to the DEVICE_PASSWORD_VERIFIER challenge in SRP authentication
/// </summary>
/// <param name="challenge">Response from the InitiateAuth challenge</param>
/// <param name="devicePassword">Password for the CognitoDevice, needed for authentication</param>
/// <param name="deviceKeyGroup">Group Key for the CognitoDevice, needed for authentication</param>
/// <param name="tupleAa">Tuple of BigIntegers containing the A,a pair for the SRP protocol flow</param>
/// <returns>Returns the RespondToAuthChallengeRequest for an SRP authentication flow</returns>
private RespondToAuthChallengeRequest CreateDevicePasswordVerifierAuthRequest(AuthFlowResponse challenge,
string deviceKeyGroup,
string devicePassword,
Tuple<BigInteger, BigInteger> tupleAa)
{
string deviceKey = challenge.ChallengeParameters[CognitoConstants.ChlgParamDeviceKey];
string username = challenge.ChallengeParameters[CognitoConstants.ChlgParamUsername];
string secretBlock = challenge.ChallengeParameters[CognitoConstants.ChlgParamSecretBlock];
string salt = challenge.ChallengeParameters[CognitoConstants.ChlgParamSalt];
BigInteger srpb = BigIntegerExtensions.FromUnsignedLittleEndianHex(challenge.ChallengeParameters[CognitoConstants.ChlgParamSrpB]);
if ((srpb.TrueMod(AuthenticationHelper.N)).Equals(BigInteger.Zero))
{
throw new ArgumentException("SRP error, B mod N cannot be zero.", "challenge");
}
DateTime timestamp = DateTime.UtcNow;
string timeStr = timestamp.ToString("ddd MMM d HH:mm:ss \"UTC\" yyyy", CultureInfo.InvariantCulture);
var claimBytes = AuthenticationHelper.AuthenticateDevice(deviceKey, devicePassword, deviceKeyGroup, salt,
challenge.ChallengeParameters[CognitoConstants.ChlgParamSrpB], secretBlock, timeStr, tupleAa);
string claimB64 = Convert.ToBase64String(claimBytes);
Dictionary<string, string> srpAuthResponses = new Dictionary<string, string>(StringComparer.Ordinal)
{
{CognitoConstants.ChlgParamPassSecretBlock, secretBlock},
{CognitoConstants.ChlgParamPassSignature, claimB64},
{CognitoConstants.ChlgParamUsername, username },
{CognitoConstants.ChlgParamTimestamp, timeStr },
{CognitoConstants.ChlgParamDeviceKey, Device.DeviceKey }
};
if (!string.IsNullOrEmpty(ClientSecret))
{
SecretHash = CognitoAuthHelper.GetUserPoolSecretHash(Username, ClientID, ClientSecret);
srpAuthResponses.Add(CognitoConstants.ChlgParamSecretHash, SecretHash);
}
RespondToAuthChallengeRequest authChallengeRequest = new RespondToAuthChallengeRequest()
{
ChallengeName = challenge.ChallengeName,
ClientId = ClientID,
Session = challenge.SessionID,
ChallengeResponses = srpAuthResponses
};
return authChallengeRequest;
}
Which requires creating the PASSWORD_CLAIM_SIGNATURE in AuthenticationHelper.AuthenticateDevice (equivalent to AuthenticationHelper.AuthenticateUser, but uses deviceKey, deviceGroupKey, and devicePass instead of normal user, pass, and userpool) :
/// <summary>
/// Generates the claim for authenticating a device through the SRP protocol
/// </summary>
/// <param name="deviceKey"> Key of CognitoDevice</param>
/// <param name="devicePassword"> Password of CognitoDevice</param>
/// <param name="deviceGroupKey"> GroupKey of CognitoDevice</param>
/// <param name="tupleAa"> TupleAa from CreateAaTuple</param>
/// <param name="saltString"> salt provided in ChallengeParameters from Cognito </param>
/// <param name="srpbString"> srpb provided in ChallengeParameters from Cognito</param>
/// <param name="secretBlockBase64">secret block provided in ChallengeParameters from Cognito</param>
/// <param name="formattedTimestamp">En-US Culture of Current Time</param>
/// <returns>Returns the claim for authenticating the given user</returns>
public static byte[] AuthenticateDevice(
string deviceKey,
string devicePassword,
string deviceGroupKey,
string saltString,
string srpbString,
string secretBlockBase64,
string formattedTimestamp,
Tuple<BigInteger, BigInteger> tupleAa)
{
var B = BigIntegerExtensions.FromUnsignedLittleEndianHex(srpbString);
if (B.TrueMod(N).Equals(BigInteger.Zero)) throw new ArgumentException("B mod N cannot be zero.", nameof(srpbString));
var salt = BigIntegerExtensions.FromUnsignedLittleEndianHex(saltString);
var secretBlockBytes = Convert.FromBase64String(secretBlockBase64);
// Need to generate the key to hash the response based on our A and what AWS sent back
var key = GetDeviceAuthenticationKey(deviceKey, devicePassword, deviceGroupKey, tupleAa, B, salt);
// HMAC our data with key (HKDF(S)) (the shared secret)
var msg = CognitoAuthHelper.CombineBytes(new[] {
Encoding.UTF8.GetBytes(deviceGroupKey),
Encoding.UTF8.GetBytes(deviceKey),
secretBlockBytes,
Encoding.UTF8.GetBytes(formattedTimestamp)
});
using (var hashAlgorithm = new HMACSHA256(key))
{
return hashAlgorithm.ComputeHash(msg);
}
}
The "key" above is generated from the following:
/// <summary>
/// Creates the Device Password Authentication Key based on the SRP protocol
/// </summary>
/// <param name="deviceKey"> Username of CognitoDevice</param>
/// <param name="devicePass">Password of CognitoDevice</param>
/// <param name="deviceGroup">GroupKey of CognitoDevice</param>
/// <param name="Aa">Returned from TupleAa</param>
/// <param name="B">BigInteger SRPB from AWS ChallengeParameters</param>
/// <param name="salt">BigInteger salt from AWS ChallengeParameters</param>
/// <returns>Returns the password authentication key for the SRP protocol</returns>
public static byte[] GetDeviceAuthenticationKey(string deviceKey,
string devicePass,
string deviceGroup,
Tuple<BigInteger, BigInteger> Aa,
BigInteger B,
BigInteger salt)
{
// Authenticate the password
// u = H(A, B)
byte[] contentBytes = CognitoAuthHelper.CombineBytes(new[] { Aa.Item1.ToBigEndianByteArray(), B.ToBigEndianByteArray() });
byte[] digest = CognitoAuthHelper.Sha256.ComputeHash(contentBytes);
BigInteger u = BigIntegerExtensions.FromUnsignedBigEndian(digest);
if (u.Equals(BigInteger.Zero))
{
throw new ArgumentException("Hash of A and B cannot be zero.");
}
// x = H(salt | H(deviceGroupKey | deviceKey | ":" | devicePassword))
byte[] deviceContent = CognitoAuthHelper.CombineBytes(new byte[][] { Encoding.UTF8.GetBytes(deviceGroup), Encoding.UTF8.GetBytes(deviceKey),
Encoding.UTF8.GetBytes(":"), Encoding.UTF8.GetBytes(devicePass)});
byte[] deviceHash = CognitoAuthHelper.Sha256.ComputeHash(deviceContent);
byte[] xBytes = CognitoAuthHelper.CombineBytes(new byte[][] { salt.ToBigEndianByteArray(), deviceHash });
byte[] xDigest = CognitoAuthHelper.Sha256.ComputeHash(xBytes);
BigInteger x = BigIntegerExtensions.FromUnsignedBigEndian(xDigest);
var gModPowXn = BigInteger.ModPow(g, x, N);
// Use HKDF to get final password authentication key
var intValue2 = (B - k * gModPowXn).TrueMod(N);
var s_value = BigInteger.ModPow(intValue2, Aa.Item2 + u * x, N);
HkdfSha256 hkdfSha256 = new HkdfSha256(u.ToBigEndianByteArray(), s_value.ToBigEndianByteArray());
return hkdfSha256.Expand(Encoding.UTF8.GetBytes(DerivedKeyInfo), DerivedKeySizeBytes);
}
This ultimately results in a invalid user/password response when calling this code (shown above):
#region Device Password Verifier
var devicePasswordChallengeRequest = CreateDevicePasswordVerifierAuthRequest(deviceAuthResponse, srpRequest.DeviceGroupKey, srpRequest.DevicePass, tupleAa);
verifierResponse = await Provider.RespondToAuthChallengeAsync(devicePasswordChallengeRequest).ConfigureAwait(false);
#endregion
Let me know if you need the DEVICE_SRP_AUTH code - I assume my issue lies somewhere in a)how I'm generating the original salt and passwordVerifier (although this doesn't return any errors when confirming the device), or how I'm generating the claim signature to pass back to DEVICE_PASSWORD_VERIFIER. The style of all of this code is pretty close to the existing logic, so it should be relatively easy for you guys to follow.
Looks like the code on my is working now! Let me know if you want me to clean it up and create a pull request.
Hi @dcolclazier, I would gladly take a pull request on that.
Any news on this issue?
Hi @dcolclazier,
The fix should have been implemented in Amazon.Extensions.CognitoAuthentication 2.0.3. Could you please verify and confirm?
Thanks, Ashish
This issue has not recieved a response in 2 weeks. If you want to keep this issue open, please just leave a comment below and auto-close will be canceled.
Hi ,
I am on v2.2.1 but seem to be running into the NotAuthorizedException: Incorrect username or password issue.
user = new CognitoUser(username, clientId, userPool, provider, clientSecret);
CognitoDevice device = new CognitoDevice(new Amazon.CognitoIdentityProvider.Model.DeviceType()
{
DeviceKey = "ap-southeast-1_abcd1234"
}, user);
user.Device = device;
InitiateSrpAuthRequest authRequest = new InitiateSrpAuthRequest()
{
Password = password,
DeviceGroupKey = "-CCPW2e2I",
DevicePass = "GW4nsddDCY9hHNObanWWMj1BbbDsoV8eThJWue+tjW6cnt/gdiHMgw==",
};
authResponse = user.StartWithSrpAuthAsync(authRequest).Result;
The device(s) are already set to Remember in the Cognito console. Please advise, thanks! :)
Hi ,
I am on v2.2.1 but seem to be running into the NotAuthorizedException: Incorrect username or password issue.
user = new CognitoUser(username, clientId, userPool, provider, clientSecret); CognitoDevice device = new CognitoDevice(new Amazon.CognitoIdentityProvider.Model.DeviceType() { DeviceKey = "ap-southeast-1_abcd1234" }, user); user.Device = device; InitiateSrpAuthRequest authRequest = new InitiateSrpAuthRequest() { Password = password, DeviceGroupKey = "-CCPW2e2I", DevicePass = "GW4nsddDCY9hHNObanWWMj1BbbDsoV8eThJWue+tjW6cnt/gdiHMgw==", }; authResponse = user.StartWithSrpAuthAsync(authRequest).Result;
The device(s) are already set to Remember in the Cognito console. Please advise, thanks! :)
Hi ,
I've managed to solve the issue. The solution is to
and then
I will create a separate issue to track this :)
Word of caution to those stumbling across this (closed) issue - there are threads on SO and another issue here: https://github.com/aws/aws-sdk-net/issues/1054 that all question the implementation and/or documentation related to including device authentication within the user auth flow via the .net SDK.
There are references to a helper called AuthenticationHelper(.cs) but it is not particularly clear where or what this is. It doesn't appear to be in the SDK or the extensions package and may or may not be required.
I think the fundamental problem with the device authentication with regards to the .net SDK, is a lack of documentation. I am making the assumption of course that it does actually work!
@RyanWarwick I am getting the same error as you. I tried swapping the userId
for deviceKey
but still get the Incorrect username or password.
error you were also seeing. I outlined my issue here, #139. Do you have any suggestions or an example I could reference for comparison? Thanks!
I can't seem to figure out how to properly add a newly tracked device to a CognitoUser.Device prior to logging in, so the login is tracked for that device key.
Here is the general flow, as an overview:
2: The AuthenticationResult object within the response contains a NewDeviceMetadata object, which contains a new DeviceKey (CONFIRMED)
3: The application generates a salt and password verifier and passes it to Cognito, and receives a 200 response.
5: Now, the next time we load the application, we use the device key that now exists:
6: Observe error during StartWithSrpAuthAsync() from step 5:
I've also tried calling the following after authenticating (for step 5), but receive the same error: