Flutter Dev Journal(2): AWS Cognito SDK
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 MethodChannel
s, 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.
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. Device authentication failure
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 | HTTP/1.1 400 Bad Request |
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 | - '`$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 | POST / HTTP/1.1 |
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 | POST / HTTP/1.1 |
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 | <!--Android shared preferences--> |
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 | // File authentication_helper.dart |
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 | POST / HTTP/1.1 |
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.