amplify-android icon indicating copy to clipboard operation
amplify-android copied to clipboard

UserCancelledException

Open tfreeman82 opened this issue 1 year ago • 5 comments

Before opening, please confirm:

Language and Async Model

Kotlin, Kotlin - Coroutines

Amplify Categories

Authentication

Gradle script dependencies

// Put output below this line
implementation 'com.amplifyframework:aws-auth-cognito:2.12.0'
 implementation 'com.amplifyframework:core:2.12.0'
 implementation 'com.amazonaws:aws-android-sdk-auth-userpools:2.73.0'
 implementation 'com.amazonaws:aws-android-sdk-auth-ui:2.73.0'

Environment information

# Put output below this line
------------------------------------------------------------
Gradle 8.3
------------------------------------------------------------

Build time:   2023-08-17 07:06:47 UTC

Kotlin:       1.9.0
Groovy:       3.0.17

Please include any relevant guides or documentation you're referencing

https://docs.amplify.aws/android/build-a-backend/auth/sign-in-with-web-ui/#launch-web-ui-sign-in

Describe the bug

When using Amplify.Auth.signInWithWebUI(activity) after successfully logging out, I occasionally get the UserCancelledException with no interaction with webview window.

Reproduction steps (if applicable)

  1. Call the sign out method then, within the sign out callback, call the signInWithWebUI method.
  2. See the UserCancelledException after going through this process a few times.

Note: This is not consistent as to when the exception is returned. Sometimes it takes a couple times, sometimes many more.

Code Snippet

// Put your code below this line.

fun signInWithWebUI(
        activity: Activity,
        callback: (String?, AmplifyException?) -> Unit,
    ) {
        signOut { success, error ->

            Amplify.Auth.signInWithWebUI(activity,
                {
                    fetchCurrentAuthSession(callback)
                },
                {
                    Log.e("", "Signin failed", it)
                }
            )
        }
    }

  fun signOut(callback: (success: Boolean, error: Exception?) -> Unit) {
        Amplify.Auth.signOut { signOutResult ->
            when (signOutResult) {
                is AWSCognitoAuthSignOutResult.CompleteSignOut -> {
                    Log.d("QuickstartApp", "cognito signout success")
                    callback.invoke(true, null)
                }
                is AWSCognitoAuthSignOutResult.PartialSignOut -> {
                    signOutResult.hostedUIError?.let {
                        callback.invoke(false, it.exception)
                        Log.d("QuickstartApp", "hostedUI error: ${it.exception.localizedMessage}")
                        // Optional: Re-launch it.url in a Custom tab to clear Cognito web session.

                    }
                    signOutResult.globalSignOutError?.let {
                        Log.e("QuickstartApp", "GlobalSignOut Error", it.exception)
                        callback.invoke(false, it.exception)
                        // Optional: Use escape hatch to retry revocation of it.accessToken.
                    }
                    signOutResult.revokeTokenError?.let {
                        Log.e("QuickstartApp", "RevokeToken Error", it.exception)
                        callback.invoke(false, it.exception)
                        // Optional: Use escape hatch to retry revocation of it.refreshToken.
                    }
                }
                is AWSCognitoAuthSignOutResult.FailedSignOut -> {
                    Log.d("QuickstartApp", "FailedSignOut: ${signOutResult.exception.localizedMessage}")
                    callback.invoke(false, signOutResult.exception)
                }
            }
        }
    }

    fun fetchCurrentAuthSession(callback: (String?, AmplifyException?) -> Unit) {
        Amplify.Auth.fetchAuthSession(
            {
                val session = it as AWSCognitoAuthSession
                val token = session.userPoolTokensResult.value?.idToken
                Log.d("QuickstartApp", "fetchCurrentAuthSession: $token")
                callback(token, null)
            },
            {
                Log.e("QuickstartApp", "Failed to fetch auth session: ${it.localizedMessage}")
                callback(null, it)
            }
        )
    }

Log output

// Put your logs below this line

Signin failed UserCancelledException{message=The user cancelled the sign-in attempt, so it did not complete., cause=null, recoverySuggestion=To recover: catch this error, and show the sign-in screen again.} at com.amplifyframework.auth.cognito.RealAWSCognitoAuthPlugin$handleWebUISignInResponse$1.invoke(RealAWSCognitoAuthPlugin.kt:928) at com.amplifyframework.auth.cognito.RealAWSCognitoAuthPlugin$handleWebUISignInResponse$1.invoke(RealAWSCognitoAuthPlugin.kt:878) at com.amplifyframework.statemachine.StateMachine$getCurrentState$1.invokeSuspend(StateMachine.kt:121) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:463) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) at java.lang.Thread.run(Thread.java:1012)

amplifyconfiguration.json

val categoryConfig = JSONObject( "{\n" + " "auth": {\n" + " "plugins": {\n" + " "awsCognitoAuthPlugin": {\n" + " "IdentityManager": {\n" + " "Default": {}\n" + " },\n" + " "CognitoUserPool": {\n" + " "Default": {\n" + " "PoolId": "${BuildConfig.AMPLIFY_POOL_ID}",\n" + " "AppClientId": "${BuildConfig.AMPLIFY_CLIENT_ID}",\n" + " "Region": "us-east-2"\n" + " }\n" + " },\n" + " "Auth": {\n" + " "Default": {\n" + " "authenticationFlowType": "USER_SRP_AUTH",\n" + " "OAuth": {\n" + " "WebDomain": "${BuildConfig.AMPLIFY_WEB_DOMAIN}",\n" + " "AppClientId": "${BuildConfig.AMPLIFY_CLIENT_ID}",\n" + " "SignInRedirectURI": "xxxxxx://signin",\n" + " "SignOutRedirectURI": "xxxxxx://signout",\n" + " "Scopes": [\n" + " "email",\n" + " "openid",\n" + " "profile",\n" + " "aws.cognito.signin.user.admin"\n" + " ]\n" + " }\n" + " }\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}" )

GraphQL Schema

// Put your schema below this line


Additional information and screenshots

No response

tfreeman82 avatar Dec 08 '23 14:12 tfreeman82

I've noticed that you are mixing Amplify v2 and our AWS Mobile SDKs. These libs are not compatible with each other and have a good chance of being the issue you are running into.

tylerjroach avatar Dec 08 '23 15:12 tylerjroach

I've removed the dependencies that weren't used and only kept the two amplify dependencies. I'm still seeing the issue.

implementation 'com.amplifyframework:core:2.12.0' implementation 'com.amplifyframework:aws-auth-cognito:2.12.0'

tfreeman82 avatar Dec 08 '23 16:12 tfreeman82

@tfreeman82 There is only 1 block in the library that triggers UserCancelledException: https://github.com/aws-amplify/amplify-android/blob/f3f8caf5ba3bbd26514ce9678cb6e161f0161c04/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt#L1058

This happens when the intent.data is null, which means we did not receive any intent data back from the web browswer..

I'm noticing that in your case, and additionally, the previous similar issue with UserCancelledException, both code examples are showing signInWithWebUI immediately being called after signOut. This makes me speculate that their could be a race condition here where if the browser is immediately reopened (It opens momentarily, even if you don't see it, to sign out the Cognito hosted ui session), it may send back the previous intent (from the sign out).

While I don't necessarily encourage the use of a sleep/wait block, if you are able to replicate this on a test device, could you wait for a short period of time (ex: 500ms) before triggering signInWithWebUI after signOut has returned? This would help pin down where the issue lies.

I'm curious of your use case to call signOut and then signIn immediately after. Is the signOut call always necessary beforehand? It would be best to check if you are already signed in first, and only sign out in that case.

tylerjroach avatar Dec 11 '23 17:12 tylerjroach

The decision to call signOut before signInWithWebUI was made because we only have a few clients that use SSO and didn't want to affect every user with the signOut process because of the way it opens the browser to handle the sign out process.

Instead, we decided to add it to the tap of the SSO sign in button so that only the users who use it see the window open and close.

With that being said, I added a 500ms delay between the signOut success and the signInWithWebUI call and no longer see the error.

tfreeman82 avatar Dec 15 '23 13:12 tfreeman82

Thanks for your response. I'll further investigate if there are improvements that can be made on our end, and where this race condition lies.

tylerjroach avatar Dec 27 '23 14:12 tylerjroach