How to avoid getting a bad token after Google-login dialog?

Viewed 233

Background

Inside the app, there is a Google-login step to register with the server using a token. This is done via the dependency of :

api('com.google.android.gms:play-services-auth:20.0.0')

The app triggers showing up to 2 dialogs for the user :

  1. Login (not shown if user has logged in for the currently installed app) :

![enter image description here

  1. Granting permissions (not shown if granted the permissions in the past):

enter image description here

This works fine for most cases.

If the user has already logged in and granted permissions, we can use the token that we got last time, assuming it's not expired. I check if it's expired using:

GoogleSignIn.getLastSignedInAccount(context)?.isExpired`) .

The problem

I've found a special problematic scenario:

  1. User has logged-in in the past and granted some permissions
  2. User went to Google-account-manager and revoked the access to the app (here).
  3. User tried to login again (for example after removal of the app)

On this case, I get a bad token that can't be used. It's actually the exact same token that I got from before revoking the access. It's probably using a cached token from last time, to avoid un-needed communication with Google server.

In this case, the server (of the SDK I work on) will send me an error that this token is invalid (which is correct), as it tries to use it.

This is problematic and seems to me like a bug on Google's SDK (I've reported here), because the token is supposed to work, as the user has re-logged in using the login-dialog, as everything was reset.

What I've tried

I tried to use various API functions, but none of them seem to tell me if the token is valid, or let me request a new token for login dialog in case the current one is invalid.

The only workaround for this that I've found, is that after a single login-dialog, and detecting that there is an error with the token (got it via the server), I choose to logout and re-login entirely:

@WorkerThread
fun logout(context: Context, googleClientId: String) {
    val options =
        GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestServerAuthCode(googleClientId)
            .requestEmail()
            .build()
    val signInClient = GoogleSignIn.getClient(context, options)
    val lastSignedInAccount = GoogleSignIn.getLastSignedInAccount(context)
    if (lastSignedInAccount != null) {
        Tasks.await(signInClient.revokeAccess())
        Tasks.await(signInClient.signOut())
    }
}

Only after that, I can login using an Intent that I prepare:

@WorkerThread
fun prepareIntent(context: Context, googleClientId: String): Intent {
    val options =
        GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestServerAuthCode(googleClientId)
            .requestEmail()
            .build()
    val signInClient = GoogleSignIn.getClient(context, options)
    val lastSignedInAccount = GoogleSignIn.getLastSignedInAccount(context)
    if (lastSignedInAccount?.isExpired == true) {
        var success = false
        kotlin.runCatching {
            val result: GoogleSignInAccount? = Tasks.await(signInClient.silentSignIn())
            success = result?.isExpired == false
        }
        if (!success)
            kotlin.runCatching {
                Tasks.await(signInClient.revokeAccess())
                Tasks.await(signInClient.signOut())
            }
    }
    return signInClient.signInIntent
}

This isn't a nice thing to do, because the user sees 3 dialogs instead of up to just 2 dialogs as I've shown in the beginning :

  1. Login
  2. Grant permissions
  3. Login again, as the token was invalid.

The questions

  1. How can I avoid 2 login-dialogs ?

  2. Is there an API that forces getting a new token for login dialog?

  3. Is this a known bug, and this is the only workaround I can indeed use?

1 Answers

It is not a bug - this behavior is intentional and you will have to handle it properly.

According to the google official user consent policy if your users have revoked access to some of the features you will have to ask them once more.

What does it mean in terms of UI/UX of your app? Well, it depends. If the features you need to access are on some separate rarely accessible pages - you can force users to relogin only when they enter those pages. If your entire app depends on those features - you would have to force relogin users on your apps enter.

The important thing to understand is that you will have to relogin users and ask them once more for their consent, no matter what - there is no other way. You cannot get a new token silently if the consent was revoked. It is an intentional limitation.

Regarding two dialogs - you cannot avoid it if some of the scopes are consensual.

Regarding the actual implementation, there are several ways.

  1. The easiest way is to wait for the access error for the requests with the invalid tokens and on error relogin user.
  2. The harder but more user/developer friendly method is to check the authorized scopes of your token via https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=YOUR_TOKEN(I am not sure if the SDK has this function, also there are v1 and v3 versions of this request.) which will give you the response something like { "audience":"", "user_id":"", "scope":"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", "expires_in":0 } where the scope field is a space-separated list of current token scopes. If there is no scope you need for your current feature - just relogin the user with the needed scope.

Of course, the approach only works for the scoped access rights for the generic ones(as background location disclosure or specific analytics collection) you will have to wait for the error.

Related