I’ve been messing around with the Gmail API recently at work and on a personal project, and I came up with a clever way to get inbox access for a single Google account. This solution I came up with felt non-obvious so I thought I’d share it here to save somebody out there some time and headaches.

Google Cloud describes two ways to get access to a specific Gmail inbox. The first is by setting up a new OAuth 2.0 client in the GCP console. Here you can configure what scopes you want to request from consenting users when they login to your applications. There are three types of scopes: non-sensitive, sensitive, and restricted. Non-sensitive scopes generally give you access to information that is publicly available on the user’s Google profile like their name or avatar. Sensitive and restricted scopes give you access to information and capabilities that could very easily be used to destroy someone’s life, and Google rightfully locks these down pretty tightly.

To use sensitive/restricted scopes, you have three options:

First, you can use them if your OAuth application is kept in a “testing” state in GCP. This simply means Google lets you use the scopes, but you can only add ~100 emails to your application’s authorized test user list. Users are then shown a warning message when logging in that says your application hasn’t been verified by Google yet and that your application is in a testing state. If you want to open this up so any Google user on the internet can sign up and use your application, Google requires you to go through a Cloud Application Security Audit (CASA) performed by a partner of theirs. It’s about $400 (+ a couple weeks of waiting) for a sensitive scope audit and even more for a restricted scope audit. It's a necessary, but annoying step if you’re launching a public facing app.

Second, you can set your OAuth application in GCP to be internal only. This means that only people from your Google Workspace domain can sign in and use the app. They aren’t shown the verification warning and you don’t have to do CASA. This route prevents anyone without the same Google workspace domain from using your app.

Third, you can turn on domain wide delegation in Google workspace for a service account created in GCP. This means that a service account can silently access any user’s inbox across your entire Google workspace organization. Honestly, not a great option for privacy concerns and a bit too Orwellian feeling for my taste.

None of the options above were going to work for my use case. I needed to silently access a single Gmail inbox. And this inbox isn’t an actual user of my application so I didn’t need to get inbox read consent from real users of my application. It’s more so like a service account, acting as a catch all for emails that I want to ingest into my application. I forward important emails to this inbox, then my application reads the emails, embeds their content, and saves them into my system.

As I was researching how I was going to achieve this using Google’s authentication functionality, I had an idea to take advantage of the refresh tokens that Google issues when you authenticate using the OAuth code flow. Google provides convenient functionality in their SDKs to implement the OAuth code flow, but you can also trigger it manually using your browser and command line. When doing it manually, you can request any type of scope for a specific user account that you have access to. Then Google will issue you a short lived access token good for one hour, and a refresh token that has a functionally infinite lifetime. By manually authenticating and saving the refresh token as an environment variable in your application, I realized I could request an access token with the scopes I needed on demand, whenever I wanted. This would let me get access to the inbox without being limited to the three access pattern options I laid out above.

Let’s walk through what the manual OAuth request looks like, and then how to use the refresh token it produces to request a fresh access token whenever you want.

  1. Create an OAuth Client in the GCP dashboard

    Click the Menu -> APIs & Services -> OAuth Consent Screen. Once there, click on Clients in the left nav menu. Then click Create client at the top of the page. Give your client a name, and select Desktop app as the application type. Download the JSON key and save the client ID and client secret somewhere safe.

  2. Create the OAuth code request

    Paste the following URL into your browser. You will be taken to a “Sign in with Google” page. Select the right account and click through the prompts. Make sure to update your client ID and the scopes. I’m just using the gmail.readonly scope since that’s all I need.

    https://accounts.google.com/o/oauth2/v2/auth?client_id={MY_CLIENT_ID}&redirect_uri=http://localhost&scope=https://www.googleapis.com/auth/gmail.readonly&response_type=code&access_type=offline&prompt=consent

  3. You’ll be redirected to a blank screen with an updated URL. It will look something like this

    http://localhost/?code=4/0AVGzR1Cr-en.......&scope=https://www.googleapis.com/auth/gmail.readonly

    Copy the full value for the code query parameter. We’ll use this in the next request to get our access and refresh tokens.

  4. Open a terminal, and make a curl request to get an access token and refresh token using the code below where code is the code from the previous step, client_id is the OAuth Client ID, and client_secret is the client secret you saved from step one. The client secret is also in the json key file you downloaded.

    curl -s
    --request POST
    --data-urlencode "code=4/0AVM...(truncated)"
    --data-urlencode "client_id=40089...(truncated)...googleusercontent.com"
    --data-urlencode "client_secret=GOCS...(truncated)"
    --data-urlencode "redirect_uri=http://localhost"
    --data-urlencode "grant_type=authorization_code"
    https://oauth2.googleapis.com/token

    The curl request will give a response that has an access token good for one hour and a refresh token. Copy and save the refresh token somewhere safe.

    {
        "access_token": "ya29.A0AS3H6N.....(truncated)",
        "expires_in": 3599,
        "refresh_token": "1//05h0kifeiv...(truncated)",
        "scope": "https://www.googleapis.com/auth/gmail.readonly",
        "token_type": "Bearer"
    }

    The refresh token is good indefinitely but will expire if you change the password for the underlying Google account or revoke privileges to the requesting application from your Google security dashboard. The refresh token should be treated as a highly sensitive credential. Do not share over messages, email, commit to repositories, etc. Set it as an environment variable in your application or put it in your secret manager.

  5. Use the refresh token in your application code to request an access token. The credentials object contains the access token.

    credentials = Credentials(
        token=None,  # Dont need to provide an initial access token.
        refresh_token=refresh_token,
        token_uri=token_uri,
        client_id=client_id,
        client_secret=client_secret
    )

Now that you have a refresh token, you can use it in your application code to get an access token required to access sensitive data for a particular Google account. I’m running a CRON job every five minutes to check the shared inbox for new content. Each time the CRON runs, I request a fresh access token and use that in subsequent requests to the Gmail API. This pattern ends up being a much more convenient way to access sensitive data from a single, non-user operated Google account without having to jump through hoops required by Google’s OAuth flow. Simple is better!