pythongoogle-cloud-platformgoogle-formsgoogle-forms-api

Google Forms API raises google.auth.exceptions.RefreshError: 'No access token in response.'


I want to create a grading bot for my community's applications through Google Forms and when I try to retrieve a form, I get a RefreshError.

Minimal Reproducible Example:

# Google Credentials

from google.oauth2 import service_account

SCOPES = ['https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/forms.body'][0]
SERVICE_ACCOUNT_FILE = 'key.json'
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)

import googleapiclient.discovery

forms = googleapiclient.discovery.build('forms', 'v1', credentials=credentials)
form_id = '__omitted__'
result = forms.forms().get(formId=form_id).execute()
print(result)

Log:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/oauth2/_client.py", line 323, in jwt_grant
    access_token = response_data["access_token"]
                   ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
KeyError: 'access_token'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "__omitted__", line 33, in <module>
    result = forms.forms().get(formId=form_id).execute()
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/googleapiclient/_helpers.py", line 130, in positional_wrapper
    return wrapped(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/googleapiclient/http.py", line 923, in execute
    resp, content = _retry_request(
                    ^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/googleapiclient/http.py", line 191, in _retry_request
    resp, content = http.request(uri, method, *args, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google_auth_httplib2.py", line 209, in request
    self.credentials.before_request(self._request, method, uri, request_headers)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/auth/credentials.py", line 156, in before_request
    self.refresh(request)
    ^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/oauth2/service_account.py", line 438, in refresh
    access_token, expiry, _ = _client.jwt_grant(
                              ^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/oauth2/_client.py", line 328, in jwt_grant
    six.raise_from(new_exc, caught_exc)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 3, in raise_from
google.auth.exceptions.RefreshError: ('No access token in response.', {'id_token': '__omitted__'})

The service account has a group of Owner with Forms API enabled.


Solution

  • Google Workspace services (including Form) represent content that includes user-owned data. For this reason, Google employs stricter controls on data access to these services and you can't (unlike Google Cloud services) use arbitrary (see below) Service Accounts to access these services.

    Confusingly (!) Cloud IAM (manifest as an IAM role Owner) is not implemented for Workspace services and role bindings have no effect. Workspace services rely on (the more limited) use of OAuth scopes.

    The recommended solution only applies to paid|business plans for Workspace and requires the creation of a Service Account that has been delegated for domain-wide authority (this link is probably the most relevant but this is poorly documented and explained). Even with this specially constructed Service Account you must impersonate a specific user in order to access their content.

    The hacky solution (caveat developer) that does not work in all cases involves adding an arbitrary Service Account's email address (generally {account}@{project}.iam.gserviceaccount.com) to specific Workspace documents (e.g. a specific Forms document) as you would to share with a human user.

    Corollary:

    1. To automate (e.g. bot) access to Google services, Service Accounts are strongly encouraged (human/user accounts may fall foul of Google's security measures).
    2. To automate Workspace services, you should use Service Accounts but the only approved way to do this is to use a domain-wide delegated Service Account which requires a paid|business Workspace plan.
    3. Arbitrary Service Accounts may work with free Workspace accounts but this requires sharing Workspace documents with the Service Account's email address

    Ergo: Google discourages non-extension-based automation of its free Workspace services.