Google OAuth tokens randomly stop working

Viewed 141

I have an application that is available on iOS, Android, and Desktop (via Electron) which all have features that depend on a Google OAuth flow.

I decided to make all of the apps redirect to an Angular web app for completing the OAuth journey which then successfully redirects back into whichever app the user came from.

Just to clarify immediately, my app is successfully verified by Google and it is set to production mode.

These are scopes which I use:

'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.phonenumbers.read',
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/contacts.readonly',
'https://www.googleapis.com/auth/contacts.other.readonly',
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/youtube.force-ssl',

Below is a step-by-step breakdown of the OAuth journey:

  1. User clicks on UI within my frontend app to connect their Google account.
  2. Frontend calls my backend (PHP Laravel) to generate a URL where the user can authenticate.
  3. Frontend sends the user to the URL in their default web browser.
  4. User enters their Google account credentials and gives consent to all the permissions/scopes.
  5. User is sent to the configured redirect URI which is an Angular web app that captures the auth code.
  6. Angular web app sends the auth code to my backend to validate the OAuth process such as generating refresh/access tokens etc.
  7. My backend responds to the Angular app successfuly.
  8. Angular app finally proceeds to redirect the user back to the app which they come from such as iOS, Android, or Desktop.
  9. User is now authenticated and the OAuth process is complete while all the Google tokens are persisted safely in my backend.

All of the above works perfectly fine across all of the apps.

Later when the user is interacting with a feature which depends on Google such as for example their Google Calendar Events, my frontend calls my backend which in turn calls Google's REST APIs using that user's persisted access token and then serves it back to the frontend app.

Important Note:

  • Since as you can see above that the OAuth process is all done in the Angular web app, I did not need to implement the Google OAuth SDKs on my iOS, Android, or Desktop apps.
  • However on the Google API Dashboard I had to generate 3 separate web OAuth clients, one for each app. This should have not been necessary since I can set multiple Redirect URIs for 1 web OAuth client but I was facing a weird bug which seemingly did not go away until I separated them.
  • So for example when iOS goes through the OAuth flow, the client ID for the iOS web client is used and so on.

Image of 3 web clients on Google API Dashboard

So what is the problem?

For some reason at random the OAuth credentials stop working. Sometimes it takes weeks to happen, sometimes days. I've been trying to find a pattern of what causes it and the closest clue I have is if the user performs SSO/OAuth across the different frontend apps perhaps. So for example they do it on Desktop and then later also perform an SSO login on their iOS app.

Or is it possible that Google is noticing this OAuth user originating from multiple different seemingly web origins so it's randomly revoking the tokens?

Everywhere I look online people mention that refresh tokens on a verified app in production mode should not expire unless the user's password is reset or they revoke the app manually, all of which are not the case for me.

Lastly to note, the same exact procedure is used with Microsoft and it works without this problem occurring. So it's specifically happening with Google OAuth only.


UPDATE (30/06/2022)

At some point for some reason randomly, my backend is unable to refresh the access token at server side. It throws the following error message when attempting to refresh the access token:

{'error' => 'unauthorized_client', 'error_description' => 'Unauthorized'}

1 Answers

When you generating access and refresh token, you have to store it safely.

The access token have a short lifetime (1 hour). You have to control if it stay valid before using it. To do it, you may use this endpoint: https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=[ACCESS_TOKEN]

If the token is valid, you will have this kind of response:

{
  "azp": "15[...]2b.apps.googleusercontent.com",
  "aud": "15[...]2b.apps.googleusercontent.com",
  "sub": "10[...]71",
  "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/business.manage",
  "exp": "1658865502",
  "expires_in": "3067",
  "access_type": "offline"
}

Else, you will have an error:

{
  "error_description": "Invalid Value"
}

You may generate a new access token from the refresh token when the expires_in value is low or when the access token became invalid. For example, with Axios:

axios.post('https://oauth2.googleapis.com/token', {
  client_id: YOUR_GOOGLE_CLIENT_ID,
  client_secret: YOUR_GOOGLE_CLIENT_SECRET,
  refresh_token: USER_GOOGLE_REFRESH_TOKEN,
  grant_type: 'refresh_token',
});

You will get a new access token:

{
    "access_token": "ya[...]63",
    "expires_in": 3599,
    "scope": "https://www.googleapis.com/auth/business.manage https://www.googleapis.com/auth/userinfo.profile",
    "token_type": "Bearer",
    "id_token": "ey[...]bA"
}

If the authentication was revoked by the user, you will have an error in response:

{
    "error": "invalid_grant",
    "error_description": "Token has been expired or revoked."
}
Related