Detecting when Google API's oAuth token has been revoked

Viewed 5052

I'm using Google's python quickstart script from here: https://developers.google.com/calendar/quickstart/python

It works great under normal circumstances. I can allow it access to my Gmail account, and it can create Google Calendar entries.

If I login to my Gmail account in a browser however, go to Security, and revoke my app's access to my Google account, the quickstart script doesn't pick up on it. It just crashes:

$ python quickstart.py
Getting the upcoming 10 events
Traceback (most recent call last):
  File "quickstart.py", line 52, in <module>
    main()
  File "quickstart.py", line 42, in main
    orderBy='startTime').execute()
  File "/opt/my_project/venv3/lib/python3.6/site-packages/googleapiclient/_helpers.py", line 130, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "/opt/my_project/venv3/lib/python3.6/site-packages/googleapiclient/http.py", line 851, in execute
    method=str(self.method), body=self.body, headers=self.headers)
  File "/opt/my_project/venv3/lib/python3.6/site-packages/googleapiclient/http.py", line 165, in _retry_request
    resp, content = http.request(uri, method, *args, **kwargs)
  File "/opt/my_project/venv3/lib/python3.6/site-packages/google_auth_httplib2.py", line 213, in request
    self.credentials.refresh(self._request)
  File "/opt/my_project/venv3/lib/python3.6/site-packages/google/oauth2/credentials.py", line 136, in refresh
    self._client_secret))
  File "/opt/my_project/venv3/lib/python3.6/site-packages/google/oauth2/_client.py", line 237, in refresh_grant
    response_data = _token_endpoint_request(request, token_uri, body)
  File "/opt/my_project/venv3/lib/python3.6/site-packages/google/oauth2/_client.py", line 111, in _token_endpoint_request
    _handle_error_response(response_body)
  File "/opt/my_project/venv3/lib/python3.6/site-packages/google/oauth2/_client.py", line 61, in _handle_error_response
    error_details, response_body)
google.auth.exceptions.RefreshError: ('invalid_grant: Token has been expired or revoked.', '{\n  "error": "invalid_grant",\n  "error_description": "Token has been expired or revoked."\n}')

If I add some debug lines just before the line that throws the exception, I see this:

creds.valid: True
creds.expired: False

I know I can just catch google.auth.exceptions.RefreshError. The problem I have is that there's an entire section of code at the beginning of quickstart.py whose purpose is to detect that the token is valid, but it appears to fail to detect this fairly simple scenario, and the exception isn't thrown until I actually try to use the token to execute a command. Putting every single execute (or at least the first one) in a try-except block in case the first part of the script couldn't do its job in detecting that the token is revoked seems dumb.

Is there a way to detect that the certificate was revoked before I actually try to use it? Why doesn't creds.valid and creds.expired work?

Update: It looks like if you wait long enough (a few minutes or hours - not sure), creds.valid and creds.expired will eventually show that the credentials are no longer valid. This time in between however, when the credentials appear to be valid, but can't be used, is enough to make my program crash if I don't handle it correctly.

Update 2: The only thing I can think of is to just run something like:

from googleapiclient.discovery import build
from google.auth.exceptions import RefreshError

...
try:
    service = build('calendar', 'v3', credentials=creds)
    service.events().list(calendarId='primary', maxResults=1).execute()
except RefreshError:
    ...
...

right after the code that checks if the credentials are valid. Sort of like a final check. This works, but seems a bit dirty, as it requires an extra call to the Google API servers, and is basically exactly what I said above - putting the first execute in a try-except block. Is there no better way?

1 Answers

The quickstart is for a simple case with your own credentials. They clearly do not expect you to revoke your own authorization. Instead of refreshing the credentials only if they have expired refresh the access token at the start every time. That first refresh will be the failure point in case the refresh token has been revoked.

    # If there are no (valid) credentials available, let the user log in.
    if creds and creds.refresh_token:
        try:
            creds.refresh(Request())
        except RefreshError:
            logger.error("Credentials could not be refreshed, possibly the authorization was revoked by the user.")
            os.unlink('token.pickle')
            return
    else:
        flow = InstalledAppFlow.from_client_secrets_file(
            'credentials.json', SCOPES)
        creds = flow.run_local_server(port=0)
    # Save the credentials for the next run
    with open('token.pickle', 'wb') as token:
        pickle.dump(creds, token)
Related