'Gmail API service account unauthorized_client error even with domain-wide authority
I have read extensively on how to access GCP Gmail API using service account and have given it domain-wide authority, using the instruction here:
https://support.google.com/a/answer/162106
Here is my service account:
Here is the scopes added to the domain-wide authority. You can see that the ID matches the service account.
One thing I notice is that my GCP project is an internal project, I havent' published it or anything, yet when I added the scope, it is not showing the service account email name but the project name. Does it make any difference? Do I need to set anything here? In the OAuth Consent Screen, I see the name of the project is being defined there. I have added all same scope on this screen too, not sure if it make any difference.
Here is my code:
from google.oauth2 import service_account
from googleapiclient import discovery
credentials_file = get_credentials('gmail.json')
scopes = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.labels', 'https://www.googleapis.com/auth/gmail.modify']
credentials = service_account.Credentials.from_service_account_info(credentials_file, scopes=scopes)
delegated_credentials = credentials.with_subject("[email protected]")
GMAIL_SERVICE = discovery.build('gmail', 'v1', credentials=delegated_credentials)
labels = GMAIL_SERVICE.users().labels().list(userId='me').execute()
Error message:
Google.auth.exceptions.RefreshError: ('unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.', {'error': 'unauthorized_client', 'error_description': 'Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.'})
Solution 1:[1]
Not sure I can answer precisely on the original question (I think not), but here how things are done in cloud functions developed by me. The following particular code snippet is written/adopted for this answer, and it was not tested:
import os
import google.auth
import google.auth.iam
from google.oauth2 import service_account
from google.auth.exceptions import MutualTLSChannelError
from google.auth.transport import requests
import googleapiclient.discovery
from google.cloud import error_reporting
GMAIL_SERV_ACCOUNT = "A service account which makes the API CALL"
OAUTH_TOKEN_URI = "https://accounts.google.com/o/oauth2/token"
GMAIL_SCOPES_LIST = ["https://mail.google.com/"] # for example
GMAIL_USER = "User's email address, who's email we would like to access. [email protected] - from your question"
# inside the cloud function code:
local_credentials, project_id = google.auth.default()
local_credentials.refresh(requests.Request())
signer = google.auth.iam.Signer(requests.Request(), local_credentials, GMAIL_SERV_ACCOUNT)
delegate_credentials = service_account.Credentials(
signer, GMAIL_SERV_ACCOUNT, OAUTH_TOKEN_URI, scopes=GMAIL_SCOPES_LIST, subject=GMAIL_USER)
delegate_credentials.refresh(requests.Request())
try:
email_api_service = googleapiclient.discovery.build(
'gmail', 'v1', credentials=delegate_credentials, cache_discovery=False)
except MutualTLSChannelError as err:
# handle it somehow, for example (stupid, artificial)
ER = error_reporting.Client(service="my-app", version=os.getenv("K_REVISION", "0"))
ER.report_exception()
return 0
So, the idea is to use my (or 'local') cloud function's service account to create credentials of a dedicated service account (GMAIL_SERV_ACCOUNT - which is used in many different cloud functions running under many different 'local' service accounts); then use that 'delegate' service account to get API service access.
I don't remember if the GMAIL_SERV_ACCOUNT should have any specific IAM roles. But I think the 'local' cloud function's service account should get roles/iam.serviceAccountTokenCreator
for it.
Updated:
Some clarification on the IAM role. In terraform (I use it for my CICD) for a given functional component, it looks:
# this service account is an 'external' for the given functional component,
# it is managed in another repository and terraform state file
# so we should get it at first
data "google_service_account" "gmail_srv_account" {
project = "some project id"
account_id = "actual GMAIL_SERV_ACCOUNT account"
}
# now we provide IAM role for that working with it
# where 'google_service_account.local_cf_sa' is the service account,
# under which the given cloud function is running
resource "google_service_account_iam_member" "iam_token_creator_gmail_sa" {
service_account_id = data.google_service_account.gmail_srv_account.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${google_service_account.local_cf_sa.email}"
depends_on = [
google_service_account.local_cf_sa,
]
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |