Search code examples
pythonpush-notificationgmailgmail-apigoogle-cloud-pubsub

GmailAPI: "Error sending test message to Cloud PubSub projects/[project-id]/topics/[topic-id] : User not authorized to perform this action."?


I am working with the Gmail API, and am trying to set up push notifications using Python 3.9. I am getting an error when I am trying to call watch() on my Gmail inbox, even though I have followed all advice given on similar questions. The error reads:

"Error sending test message to Cloud PubSub projects/[project-id]/topics/[topic-id] : User not authorized to perform this action."


Thus far, I have done the following:

  • Create a topic
  • Create a subscription
  • Manually send and receive a message
  • Create a service account, with permissions "Owner" and "Cloud Pub/Sub Service Agent"
  • For the topic, set the service account as Owner
  • For the subscription, set the service account as Owner

In terms of the code, I simply added onto the Python Quickstart file (https://developers.google.com/gmail/api/quickstart/python) supplied by Google. Here is what I have added:

from __future__ import print_function
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def main():
    """Shows basic usage of the Gmail API.
    Lists the user's Gmail labels.
    """
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        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.json', 'w') as token:
            token.write(creds.to_json())

  
    #NEW CODE I ADDED FOR watch()
    service = build('gmail', 'v1', credentials=creds)

    request = {
        'labelIds': ['INBOX'],
        'topicName': 'projects/[project-id]/topics/[topic-id]'
    }

    service.users().watch(userId='me', body=request).execute()

if __name__ == '__main__':
    main()



EDIT:

  1. I am running it on a Windows laptop, in Visual Studio Code. I simply opened up a new Python file.
  2. It is an @gmail.com account, not a GSuite account
  3. I'm using a service account to authenticate (the creds object)

EDIT 2: I added the full file with the credentials above.


Solution

  • Updated answer

    I've tested your code, and I encountered the same issue, but this was because I purposfully skipped steps to figure out what happens at certain stages.

    I got your exact issue when I did not add the service account gmail-api-push@system.gserviceaccount.com as a publisher on the topic in question.

    Excuse my horrible paint skills: enter image description here


    Original answer

    So the issue is that you're trying to use a service account to authenticate an API call to the Gmail API for an @gmail.com account, I'm guessing following this example.

    The only way this would work is if the account you're using is a GSuite account and you're using Domain Wide Delegation. This doc (for the Admin SDK, but the way it works is the same) explains how that would work. It's not applicable for you, but I'm adding this for completeness' sake.

    Service accounts cannot be used to authenticate requests for @gmail.com accounts.

    They can be used to authenticate their own GSuite related APIs, such a GDrive, GDocs, etc. but then they are basically used to authenticate their own account, never an @gmail.com account. I won't go into details for this as this is not exactly relevant, and it's a bit of a weird rabbithole that even Alice wouldn't be able to get out of.

    To use the Gmail API with your @gmail.com account, you'll need to create the creds object as indicated in the quickstart you linked so the request gets authenticated using the credentials for your @gmail.com account. You'll only need to do this once, and save the resulting JSON credentials file somewhere so you can reuse it. It will stay valid until you revoke it.

    Note that the docs also state that you need to use standard Gmail API authentication.


    The part below creates the JSON file which you can use to authenticate the requests to the Gmail API using the @gmail.com account you did the login with.

            # Save the credentials for the next run
            with open('token.json', 'w') as token:
                token.write(creds.to_json())
    

    You can use this part to then load the credentials into a creds object, and then simply using the code you already have should work...

        if os.path.exists('token.json'):
            creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    

    I'm not 100% sure if you will need to give permission on the GCloud project itself, but you can test if the Gmail creds object works by finishing the rest of the quickstart and see if you can get the labels for the @gmail.com account. If this works, and you're still getting a 403 error, try giving the @gmail.com account the Pub/Sub IAM permissions you gave the service account.