On this page

Chapter 3 of 8. Prerequisite: Chapter 2 — you should have a Google Cloud project with an OAuth client and a consent screen configured. If you want to skip the protocol theory entirely, Chapter 4 reads as a self-contained reference.

Why this chapter exists at all

You can use OAuth without understanding it. Google's Python library will handle the flow correctly if you call its InstalledAppFlow.run_local_server() method with the right client ID, secret, and scopes. Many people ship working automation that way.

The reason to read this chapter anyway is that when something breaks — and something will break, eventually, in the form of an expired refresh token or a scope mismatch or a stuck consent screen — the error messages assume you know what each piece is for. Once you have read the protocol once, those errors become readable. Once they are readable, the fixes are typically one line.

Four roles

RFC 6749 §1.1 defines four roles. For our automation, here is who fills each one:

  • Resource owneryou. The human whose Google account holds the GSC and GA4 data we want to query.
  • Clientthe Python script you are about to write. The application asking for access on your behalf.
  • Authorization serverGoogle's identity server. The thing that shows the consent screen and issues tokens. Lives at accounts.google.com and oauth2.googleapis.com.
  • Resource serverGoogle's API servers. The things that actually answer the GSC and GA4 queries. Live at searchconsole.googleapis.com and analyticsdata.googleapis.com.

In a multi-user app these roles are distinct entities. In a single-user automation they collapse: you are the resource owner and the author of the client and the operator of the script that uses the tokens. The protocol does not change because of that collapse; the roles are still doing what the spec says they are doing. They are just all initiated from your laptop.

Two tokens, and why both exist

OAuth issues two distinct credentials from the same consent step:

  • An access token, which the client presents on every API call as proof that the resource owner authorised this access. RFC 6750 defines how it is presented (Authorization: Bearer <token> header, by convention).
  • A refresh token, which the client holds in long-term storage and exchanges for a new access token whenever the current one expires.

Two tokens exists because of one specific risk: an access token in transit is a bearer credential — anyone who intercepts it can use it until it expires. Making access tokens short-lived (Google gives them one hour) limits the blast radius of an interception. But you do not want to ask the user to click "Allow" on a consent screen every hour. So OAuth issues a separate, long-lived refresh token that does not travel with API calls. The refresh token goes to the authorization server only, over TLS, only to mint a new access token. It never goes to the resource server.

The Authorization Code flow

For installed apps, the relevant flow is the Authorization Code grant, documented in RFC 6749 §4.1 and in Google's web-server-flow docs. The web-server framing is the right one even for a desktop script — installed apps without a public web server use a localhost loopback as the redirect URI, but the flow is otherwise identical.

The steps, in order:

1. Client constructs an authorisation URL. It includes the client ID, the requested scopes, a redirect URI (e.g. http://localhost:8090/), and a state parameter for CSRF protection. The Python library does this for you.

2. Browser opens the URL. The user signs in to Google (or is already signed in) and reaches the consent screen, which lists the requested scopes in human-readable form: "Captain Random Telemetry wants access to: view your Search Console data, view your Analytics data."

3. User clicks Allow. Google's authorization server redirects the browser to the redirect URI with a single-use ?code=… query parameter appended. This authorisation code is short-lived (Google gives it minutes) and is not itself a token — it is a one-time voucher that can be exchanged for tokens by a party that knows the client secret.

4. Client exchanges the code for tokens. It POSTs to https://oauth2.googleapis.com/token with the code, client ID, client secret, and redirect URI. Google's authorization server responds with { access_token, refresh_token, expires_in, token_type, scope }. The exchange happens once, over TLS, without going through the browser.

5. Client uses the access token. Every subsequent call to the GSC or GA4 APIs carries Authorization: Bearer <access_token> per RFC 6750. The resource servers honour it until it expires.

6. Client refreshes when needed. When the access token expires (one hour), the client POSTs to the same /token endpoint with grant_type=refresh_token plus the refresh token and client credentials. Google returns a fresh access token. No browser involvement, no user interaction.

Steps 1-4 happen exactly once per user-and-scope combination, in your case at install time. Step 5 happens on every API call. Step 6 happens about once an hour while the script is running.

The consent step (steps 2-3) is the only place where a human is in the loop. Once the user has granted consent for a set of scopes, the refresh token remains valid for that set of scopes until it is revoked — by the user, by Google, or by the client.

This is the behaviour we want for automation. The script can run unattended for months without ever showing a consent screen again, because the refresh token does the access-token mint and the access-token mint does not need the user.

It is also why adding a new scope triggers a new consent flow. Google's authorization server treats each new scope as a new grant that needs explicit user authorisation. If you start out asking for read-only GSC access and later decide you also want read-only GA4 access, the next time the script runs after the change, it will need to walk through consent again.

What revocation looks like

Refresh tokens can be invalidated in three ways:

  • The user revokes consent at myaccount.google.com under "Third-party apps with account access."
  • Google's automatic policies fire — six months of inactivity, a password reset on the account, a scope change on the OAuth client.
  • The client calls revoke() explicitly. This is rare for automation but useful for a --logout subcommand.

When any of these happens, the next refresh attempt fails with an invalid_grant error. The right response in code is to surface a clear message and prompt the user to re-run the consent flow. You cannot recover a revoked refresh token; you can only mint a new one through consent.

The thread that runs through this guide

The whole reason this guide spends a chapter on storage (chapter 4) and a chapter on the OAuth protocol (this one) is that the refresh token is the single most important credential in the whole automation. Lose it and you walk consent again. Leak it and someone else can mint access tokens against your account for as long as it stays valid.

The same principle showed up in a Telegram-bot context I wrote about earlier this year — two automation processes accidentally sharing a single bot token created an infinite 409 Conflict loop that took hours to diagnose (Two bots, one token: infinite 409 Conflict errors). The lesson generalises: tokens are infrastructure, not afterthoughts. Treat them with the discipline you would treat any other production credential.

That discipline starts with where you put them. Chapter 4 is about exactly that.

// EXERCISE

Trace your own consent flow end to end

Run the Authorization Code flow against the OAuth client you created in chapter 2, and log every protocol step as it happens so each part of RFC 6749 maps to something you observed. Capture the authorisation URL your code constructs, the localhost redirect carrying the one-time code, the token exchange response, and a later silent refresh.

Expected behaviour
  • A logged authorisation URL containing your client ID, the requested scopes, a localhost redirect URI and a state parameter
  • The localhost redirect captured with its single-use code query parameter
  • The token exchange response showing access_token, refresh_token, expires_in and scope fields
  • A later run that mints a fresh access token via grant_type=refresh_token with no browser and no consent screen

PROVE IT Run the script twice in a row and show that the first run opens a browser for consent while the second completes silently on the stored refresh token.

// CHECKPOINT — OAUTH FLOW
multiple choice · auto-checked

Why does OAuth issue both an access token and a refresh token from a single consent?

exact answer · auto-checked

When a refresh token has been revoked, the next refresh attempt fails with which error?

open · self-checked

You add the GA4 read-only scope to a script that previously asked only for GSC access. What happens on the next run, and why?

Show answer

The script has to walk the consent flow again in a browser. Google's authorization server treats each new scope as a new grant needing explicit user authorisation, so the existing refresh token does not cover the added scope. This is why the chapter advises requesting the full set of scopes you are likely to need on the first consent flow.

↺ re-read: “Why consent happens once

Lived experience

Sources

  • RFC 6749: The OAuth 2.0 Authorization Framework
    IETF
    The protocol itself
    datatracker.ietf.org
  • RFC 6750: The OAuth 2.0 Authorization Framework — Bearer Token Usage
    IETF
    How the access token is presented on each API call
    datatracker.ietf.org
  • Using OAuth 2.0 for Web Server Applications
    Google
    Google's implementation of the Authorization Code flow
    developers.google.com
  • OAuth 2.0 overview + flow taxonomy
    OAuth Working Group
    Where the various OAuth flows live in the wider protocol
    oauth.net
Back to guide overview