文章目录
  1. 1. Issues and fixes
    1. 1.1. 1. Client secret support
    2. 1.2. 2. Device authentication failure
  2. 2. Development journal
    1. 2.1. Client secret
    2. 2.2. Device authentication

When you are searching for AWS Cognito SDK that can be used in Flutter, you will soon be shocked that there’s no official support yet. According to a thread in AWS forum, one guy says:

I work on the AWS SDK team. I am not aware of any plans to support Flutter at this point. I will take this to my team as a feature request and will post back on this thread/or through a general forum announcement if our plans change in the future.

This is still true as of TODAY (October 2019).

So there are two community packages out there, which are amazon_cognito_identity_dart and flutter_cognito_plugin. The former is a pure Dart one translated from AWS official Javascript SDK, with good examples, but last active date was 7 months ago. The latter uses native Android and iOS AWS SDK by wrapping them into MethodChannels, still activily in development.

TL;DR

I chose amazon_cognito_identity_dart out of its pureness in Dart, the first try was successful. But later on found 2 major issues, all fixed in my fork. If you are not interested in the details, you can check that out and follow the usage there.

Thanks the author Jon Saw for the tremendous effort put into this Dart SDK, it saved me tons of time. I would not have been moved our native app into Flutter without this AWS Cognito SDK.

Issues and fixes

1. Client secret support

From amazon-cognito-identity-js where this Dart SDK translates from, it says

When creating the App, the generate client secret box must be unchecked because the JavaScript SDK doesn’t support apps that have a client secret.

Client secret is used to restrict access to AWS User Pool, any client that doesn’t have the client secret won’t be able to communicate to the pool.

Client secret setting

But our User Pool already has a client secret that has been running smoothly with Android and iOS native SDKs, so we don’t want to change.

Fortunately, it’s an easy fix by adding a SECRET_HASH parameter to the JSON requests. The hash generating method can be found in the AWS Android SDK.

1
2
3
4
5
6
7
8
9
///
/// Translated from library `aws-android-sdk-cognitoprovider@2.6.30` file `CognitoSecretHash.java::getSecretHash()`
///
static String calculateClientSecretHash(String userName, String clientId, String clientSecret) {
Hmac hmac = new Hmac(sha256, utf8.encode(clientSecret));
Digest digest = hmac.convert(utf8.encode(userName + clientId));
hmac.convert(digest.bytes);
return base64.encode(digest.bytes);
}

2. Device authentication failure

Cognito device remembering

If you have enabled device remembering feature in you User Pool, this amazon_cognito_identity_dart SDK will fail in response to DEVICE_PASSWORD_VERIFIER challenge, which occurs on the second time you login with the same device. Here is the error response from AWS:

1
2
3
4
5
6
7
8
HTTP/1.1 400 Bad Request
x-amzn-ErrorType: NotAuthorizedException:
x-amzn-ErrorMessage: Incorrect username or password.

{
"__type": "NotAuthorizedException",
"message": "Incorrect username or password."
}

This is because when you first time login with this device, it sends the WRONG PasswordVerifier JSON parameter in the AWSCognitoIdentityProviderService.ConfirmDevice request. This doesn’t fail, which means you will be able to login the first time, but it is sending a WRONG parameter (hash based of the combination of device key, device group key and a random string), so AWS Cognito remembers the WRONG information. Consequently, the second time when the SDK sends a parameter PASSWORD_CLAIM_SIGNATURE in the DEVICE_PASSWORD_VERIFIER phase using the CORRECT device key and device group key to generate that hash, hence the conflict. AWS Cognito would complain

hey dude, this is not what you told me before, get outa here

It sounds a lot to comprehend, but the fix is easy, change file authentication_helper.dart in amazon_cognito_identity_dart as shown below

1
2
-        '`$deviceGroupKey$username:${this._randomPassword}';
+ '$deviceGroupKey$username:${this._randomPassword}';

As you can see, there is one more backtick ` at the beginning of the string, which is copy & pasted from the original Amazon Javascript SDK. In Javascript, backtick is used to create template literal.

Development journal

Client secret

To figure out how to add client secret, I set up a Charles proxy to deciper the traffic between the my device and AWS server. Then I did a side by side comparison of the JSON reqeust between the official Android SDK and the Dart one.

I noticed there is one more parameter SECRET_HASH in the Android request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
user-agent: Dart/2.5 (dart:io)
x-amz-target: AWSCognitoIdentityProviderService.InitiateAuth
accept-encoding: gzip
host: cognito-idp.ap-southeast-2.amazonaws.com
content-type: application/x-amz-json-1.1; charset=utf-8

{
"AuthFlow": "USER_SRP_AUTH",
"ClientId": "YOUR_CLIENT_ID",
"AuthParameters": {
"DEVICE_KEY": "ap-southeast-2_DEVICE_UUID",
"USERNAME": "USER_EMAIL",
"SRP_A": "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNNNNNNNNNNNNNNNNGGGGGGGGGGGGGGGGGGGGGGGGGGG_RANDOM_STRING",
"SECRET_HASH": "FEgxHasTRmQq7SsCcBEAQB4xSyZmbl8UXm2a4QK6bA8="
},
}

Then I digged into the Android SDK source code and found that little method that uses HMAC_SHA256 algorithm to generate this secret hash. The pseudo code is HMAC_SHA256(clientSecret, userName + clientId), while clientSecret is the key.

Device authentication

This takes much more effort to fix. I have been staring at that place that has the extra backtick for a long long time but didn’t notice it. As with any backend related problem, I did a side by side comparison of the 400 error JSON request between the official Android SDK and the Dart one. Here is what it looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST / HTTP/1.1
user-agent: Dart/2.5 (dart:io)
x-amz-target: AWSCognitoIdentityProviderService.RespondToAuthChallenge
accept-encoding: gzip
x-amz-user-agent: aws-amplify/0.0.x dart
host: cognito-idp.ap-southeast-2.amazonaws.com
content-type: application/x-amz-json-1.1; charset=utf-8

{
"ChallengeName": "DEVICE_PASSWORD_VERIFIER",
"ClientId": "YOUR_CLIENT_ID",
"ChallengeResponses": {
"USERNAME": "USER_EMAIL",
"PASSWORD_CLAIM_SECRET_BLOCK": "SUPERRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR_LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNNNNNNNNNNNNNNNNGGGGGGGGGGGGGGGGGGGGGGGGGGG_RANDOM_STRING",
"TIMESTAMP": "Mon Oct 21 12:33:53 UTC 2019",
"PASSWORD_CLAIM_SIGNATURE": "3oEI2DofcVVxxxxxxFV7UXJWnnqnQYPAxxxxxxxxxx=",
"DEVICE_KEY": "ap-southeast-2_DEVICE_UUID",
"SECRET_HASH": "FEgxHasTRmQq7SsCcBEAQB4xSyZmbl8UXm2a4QK6bA8="
},
"Session": null
}

The number of parameters are the same, after closely examining where those values come from, I noticed PASSWORD_CLAIM_SECRET_BLOCK is copied from previous server response, so the only thing can go wrong is the PASSWORD_CLAIM_SIGNATURE.

Then I went into the file cognito_user.dart that generates the request, I was suspecting that the calculation of the hash in method getDeviceResponse() is not correct. I set up break points in the source file and went line by line to examine the result of every step. But soon found this is not effective, and I have no way to verify whether the algorithm itself has problem. So I went for another blackbox approach, I copied the cached device related parameters from Android SDK which resided in /data/data/app_package_name/shared_prefs/CognitoIdentityProviderDeviceCache.POOL-ID_USER-EMAIL.xml, to the Flutter one in my Android simulator /data/data/app_package_name/shared_prefs/FlutterSharedPreferences.xml

1
2
3
4
5
6
7
<!--Android shared preferences-->
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="DeviceSecret">DEVICE_GENERATED_RANDOM_STRING</string>
<string name="DeviceKey">ap-southeast-2_DEVICE_ID</string>
<string name="DeviceGroupKey">xxxxxxx</string>
</map>

With the three device parameters from the Android SDK, the Dart SDK was able to successfully go through the previously failed DEVICE_PASSWORD_VERIFIER request!! So it’s clear the method getDeviceResponse() that generates the hash is 100% correct, the problem must be in previous requests where these three parameter are involved. After some digging, the scope got narrowed down to method generateHashDevice() in file authentication_helper.dart where device secret is generated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// File authentication_helper.dart

void generateHashDevice(String deviceGroupKey, String deviceKey) {
_randomPassword = this.generateRandomString(); // Device secret, a random string
final String combinedString =
'`$deviceGroupKey$deviceKey:${this._randomPassword}';
final String hashedString = this.hash(utf8.encode(combinedString)); // BUG here!!

final String hexRandom = new RandomString().generate(length: 16);

_saltToHashDevices = this.padHex(BigInt.parse(hexRandom, radix: 16)); // Random salt

final verifierDevicesNotPadded = modPow(
this.g,
BigInt.parse(this.hexHash(_saltToHashDevices + hashedString), radix: 16),
this.N,
);

_verifierDevices = this.padHex(verifierDevicesNotPadded);
}

There are three variables that are suspicious. Firstly, _randomPassword is a random string and being sent to server in some sort of hashed form, it is confidential to the device. The second _saltToHashDevices is also locally generated random number, so also innocent. The last one _verifierDevices which is a hashed form of combinedString variable, is used as PasswordVerifier parameter in the following AWSCognitoIdentityProviderService.ConfirmDevice request is the ONLY place that can go wrong:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
user-agent: Dart/2.5 (dart:io)
x-amz-target: AWSCognitoIdentityProviderService.ConfirmDevice
accept-encoding: gzip
x-amz-user-agent: aws-amplify/0.0.x dart
host: cognito-idp.ap-southeast-2.amazonaws.com
content-type: application/x-amz-json-1.1; charset=utf-8

{
"DeviceKey": "ap-southeast-2_DEVICE_ID",
"AccessToken": "ACCESS_TOKEN_FROM_PREVIOUS_SERVER_RESPONSE",
"DeviceSecretVerifierConfig": {
"Salt": "Wexxxxxxcws=",
"PasswordVerifier": "APjfxxxxxxxxxxxxxxx+8pZC4+iyRlksk6cxxxxxv6ROt3SCnmRIqO8/rt0RSG4TP8xxxxxxJ7jUJeT2MS+008qSCFyJ0WDTbKT6VRd4zx4hID3G1GLujSAOBuOfbYxftFxNxxxx2c6XT/pG4miBHC/7vz6x6VSkrC00gH/JU+V5kE964fTVBHxxxxxxjoButqn9f/kpdqxxxxxxxxxxxxzJWDQiVQWZFH+31kFC7ATZKnseqQfWIse7/Xj1O2GxxxxxxxxddOBX6/WSNm7SDzCA5i+6NAlZ67JywtoqV3twrnlpQ5/i5km8WBhL9+yP6FYVv6s/1ICADuorxxxxxxKhxR12bblNdBxxxxxxxxxmppnLQqIKK8oCog1uikudNi3DhCBHJw6oWxxxxxxxxxxxxxxxxxxxx6xTYT8aJmhNs4L6hmPgcLfumgyw=="
},
"DeviceName": "Dart-device"
}

After another examination of combinedString variable, suddenly I found the weird backtick ` at the beginning of the string. That’s the extra backstick that makes the hash go wrong.

Problem resolved. One character deletion fix comes from several days of work… In Javascript, backtick is used to create template string, so it’s a copy & paste error.

文章目录
  1. 1. Issues and fixes
    1. 1.1. 1. Client secret support
    2. 1.2. 2. Device authentication failure
  2. 2. Development journal
    1. 2.1. Client secret
    2. 2.2. Device authentication