When using authenticationFlowType `USER_PASSWORD_AUTH` - username appears to be null meaning cached DeviceKey cannot be accessed
... which in turn leads to token refresh attempts failing with the spurious error output:
{"__type":"NotAuthorizedException","message":"Invalid Refresh Token."}
The refresh request has a JSON body similar to:
{
"AuthFlow": "REFRESH_TOKEN_AUTH",
"AuthParameters": {
"SECRET_HASH": "",
"REFRESH_TOKEN": "(redacted)"
},
"ClientId": "(redacted)",
"AnalyticsMetadata": {
"AnalyticsEndpointId": "(redacted)"
},
"UserContextData": {
"EncodedData": "(redacted)"
}
}
SECRET_HASH is empty in the actual request but should only ever be present for USER_SRP_AUTH I believe.
Looking in the shared_prefs directory, I see a CognitoIdentityProviderDeviceCache.(REDACTED).null.xml with the null part referring to a username I believe.
I haven't been able to trace through the flow of the SDK attempting to refresh the access token, but I'm assuming by the time it attempts to retrieve a cached device key, there is a username available, meaning the filename resolves to something without null.
Using USER_SRP_AUTH results in token refreshing working, but currently we can't guarantee all the requisite data for SRP auth for our application.
Context
The code initialising Amplify:
// Amplify auth initialisation/configuration
Amplify.addPlugin(AWSCognitoAuthPlugin())
Amplify.configure(AmplifyConfiguration.fromConfigFile(context.applicationContext, R.raw.amplifyconfiguration), context.applicationContext)
The configuration JSON:
{
"auth": {
"plugins": {
"awsCognitoAuthPlugin": {
"IdentityManager": {
"Default": {}
},
"CognitoUserPool": {
"Default": {
"PoolId": "(redacted)",
"Region": "(redacted)",
"AppClientId": "(redacted)"
}
},
"Auth": {
"Default": {
"authenticationFlowType": "USER_PASSWORD_AUTH"
}
}
}
}
}
}
I'll happily provide anything else that is useful and will update this issue if I discover anything else.
It's probably worth me noting:
The encryption keys that are stored in the AndroidKeyStore are also stored under the incorrect alias.
So even renaming the shared preferences file to what it should be won't fix the underlying issue as at the time of attempting to decrypt the values in the preferences, the encryption key will come back null.
@ChrisSawczukMSM Thank you for letting me know. I will look into this issue and provide updates here.
Just to update you to let you know that I am still working on this.
I got to reproduce this using amplify-auth v1.30.0, signing in user with username and password and trying to remember the device.
Even though it is mentioned that we need to use USER_SRP_AUTH for remembering the device, I still checked with USER_PASSWORD_AUTH if I can get the NPE while calling something like :
Amplify.Auth.signIn("username", "password", { result ->
if (result.isSignInComplete) {
Log.i(TAG, "Sign in succeeded")
Amplify.Auth.rememberDevice(
{ Log.i(TAG, "Remember device succeeded") },
{ Log.e(TAG, "Remember device failed with error", it) }
)
} else {..}
}, { Log.e(TAG, "Failed to sign in", it) }
)
I was able to get CognitoIdentityProviderDeviceCache.(REDACTED).null.xml that you've mentioned.
It appears that there might be some race condition. As I got it working when I hit the breakpoint in sdk where the shared prefs is populated. Surprisingly, it then started working even after clearing cache and on separate devices. It is also likely that there is AWS console config in play here.
Nevertheless, I do need few confirmation from you to know that I am in right track to find solution for your issue,
- Could you please confirm if you are using amplify library or aws sdk?
- What version of libs are you using?
- Could you elaborate on your use case with code snippet which is blocking.
- Amplify SDK I believe (
implementation 'com.amplifyframework:aws-auth-cognito:1.28.1') - 1.28.1 currently
Amplify.Auth.signIn(username, password, { signInResult ->
if (signInResult.isSignInComplete) {
Amplify.Auth.currentUser?.let { user ->
// Communicate successful sign in
} ?: run {
// Communicate unexpected error
}
} else {
// Communicate unexpected error
}
}, { authException ->
when (authException) {
is AuthException.NotAuthorizedException -> // Communicate username/password mismatch
else -> when (val underlyingException = authException.cause) {
is AmazonClientException -> if (underlyingException.cause is IOException) /* communicate network error */ else /* Communicate service error */
else -> // Communicate service error
}
}
})
In an attempt to work around the issue, I discovered that very shortly after the sign in function exits, using the escape hatch to retrieve the internal username will return a UUID and not null, so I think race condition is only sounding more and more likely.
I should have asked earlier. But could you please share your usecase that you are trying to fulfill with accessing deviceID.
After investigating, it does not seem possible to utilize device tracking APIs without USER_SRP_AUTH as mentioned in the documentation I've previously linked in the conversation.
Since Auth response calls omit the username in challenge params, username is set to null. This is expected.
However, if your goal is to use deviceIDs for tracking purpose. For example, by calling rememberDevice API in Auth, I'd suggest to use SRP auth mechanism.
Happy new year @sktimalsina, sorry for not replying sooner.
The usecase
I am currently attempting to introduce Cognito as the only mechanism for auth (authN and authZ) in an application that has previously used an OAuth2 mechanism.
To uplift accounts from this legacy auth to Cognito, we need their username and password to be able to generate a cognito identity.
My understanding is that we use a combination of AWS technologies to handle this when we sign in, as we are able to check our legacy system for a valid account and then generate the associated cognito identity iff we are using USER_PASSWORD_AUTH as with SRP based authentication, we don't have any suitable way to determine whether or not the user signing in has given us the correct credentials.
Now, ideally, we'd use USER_PASSWORD_AUTH once and then change the configuration of the SDK to USER_SRP_AUTH, to guarantee that all users will definitely be able to use SRP in the future, but the SDK is configurable only once during an application run and it would be too clunky to force the user to restart the app after a successful sign in to then sign in again using the USER_SRP_AUTH flow type.
As remembering devices is something that we want to use in other areas (such as web), and it is a setting that is applied globally, we don't have the option to turn it off for mobile.
USER_PASSWORD_AUTH is not compatible with device tracking APIs
Since Auth response calls omit the username in challenge params, username is set to null. This is expected.
This definitely appears to be the case during sign in- but by the time an access token is to be refreshed, there is an internal username available, if the internal username wasn't available (i.e. it was still null) this wouldn't be a problem. It makes it seem like the issue is a race condition rather than an intentional design choice, as I believe (although it was last year now!) when I stalled the sign in process by placing a breakpoint in the library code, I was able to get USER_PASSWORD_AUTH to function as a user would expect.
It feels like the ability for the SDK to not deterministically generate the shared preferences file name (and subsequent AndroidSecureStore key name) is what is causing this to be an issue, as when the values are stored under the right name, the SDK does the right thing in the situation and continues to do so (i.e. refreshing stale access tokens).
Though, if this configuration isn't supported, it'd be incredibly useful for sign in calls to fail, emitting a useful error, with links to documentation explaining why what you're trying to do isn't going to work.
The ideal solution
Needless to say, it'd be great if USER_PASSWORD_AUTH did just work, either with the SDK disabling device tracking functionality when the authentication flow type was USER_PASSWORD_AUTH or with it waiting for an internal username to become available before attempting to store credentials in the encrypted shared preferences values.
I feel like I should add that this currently works as expected when using the iOS SDK.
I really appreciate the help and effort going into this and I acknowledge that my domain knowledge is only very weak on the server-side workings of Cognito, especially around remembering devices and the terminology used, so I appreciate the patience and understanding!
Hi @sktimalsina just dropping by to say I still care about this, if you have any updates that'd be cool too, but no worries if not, cheers.
@ChrisSawczukMSM Apologies for not replying. Thank you for the detailed description and use cases. I agree with the points you have mentioned regarding potential race condition and the device tracking capabilities. However, due to nature of tight decoupling with underlying SDK, it is not trivial. Good news is, we are actively looking for the fix and major changes to avoid such circumstances. We will have to keep this issue open to revisit until then.
Hi @ChrisSawczukMSM we have now launched V2 of Amplify through which when you sign in you can mention which authentication mechanism you would like to use so now its not just what is specified in the configuration. You can use the options param for such a configuration. More information is here : https://docs.amplify.aws/lib/auth/signin_next_steps/q/platform/android/