gmail,
gcalendar, gsheets, gdocs, gdrive and gforms. One client registration, one consent
screen, one refresh token, one OAuth2Client handed to all six.
The server
google_server.py
scripts/live_google_check.py holds the same
constant in Python and fails if Google’s own discovery document stops agreeing
with it.
Google does publish metadata, so
await OAuth2Server.discover("https://accounts.google.com") builds the same
three endpoint fields at the cost of a network round trip at import time. What
it can never fill in is authorization_params — see below, because that is the
field the whole integration turns on.
Scopes
Every pack declares what its tools need, andscopes_for reads it back:
Read the second row as the reason
scopes_for takes tools rather than packs.
Google covers every Gmail endpoint here with gmail.modify except
threads_delete, which it puts behind full mailbox access because the delete
cannot be undone. That scope is declared on the tool, so it enters a consent
request only when you hand the tool out:
gmail_narrow_scopes.py
google_scopes.py
403 from the API, not from the token endpoint.
Each pack also names an environment variable. All six read
$GOOGLE_ACCESS_TOKEN, which is the static-token path for a script. It never
refreshes.
Wiring the packs
google_client.py
SubjectProvider,
which keeps one of these per subject.
Getting the first grant
The snippets here are a web app’s: a route, a session, a row per user. For one account of your own, your own account does the same two protocol steps from a terminal, with no route and no database. Charter refreshes a grant you already hold and never obtains one — the consent redirect,state, PKCE and the callback route are your framework’s.
Two steps of it are protocol, and they are the ones below:
google_connect.py
google_callback.py
grant.scopes is what was really issued; comparing
it against scopes_for is the only place that gap is visible before a tool call
returns 403.
OAuth2Client.from_grant(GOOGLE, grant, client_id=..., client_secret=...) turns
that grant into the provider above without spending a refresh.
Refresh behaviour
Google does not rotate refresh tokens. A refresh response carries anaccess_token, an expires_in of about an hour, and no refresh_token — so
on_refresh hands you back the same string you passed in. The row worth writing
is the one exchange() returned; on_refresh still earns its place as the hook
that tells your store the grant is alive.
Concurrent refreshes are harmless here, because nothing is invalidated by a
second one. OAuth2Client serialises them anyway — twelve parallel tool calls
wait on one refresh — which is what makes the same code safe against the
rotating servers on the other two provider pages.
A revoked grant answers invalid_grant. Revocation happens at
myaccount.google.com/permissions, or when a Workspace admin removes the app.
Charter raises CredentialError and then
stops asking for 60 seconds, because the token endpoint rate-limits per client
and one dead user must not degrade the rest. Call reset() after re-authorizing.
What fails silently
access_type=offline and prompt=consent. Without the first, Google returns
no refresh token at all. Without the second, a returning user who already
approved your app gets a token response with no refresh token in it — Google
only re-issues one on a consenting authorization. Nothing errors either way:
the access token works, and the integration dies within the hour. That is why
the lore lives on the OAuth2Server declaration, and why exchange() defaults
to expect_refresh_token=True and names both parameters when none comes back.
A hundred refresh tokens per account, per client. Google caps them and
silently invalidates the oldest when you cross the line. An app that runs the
consent flow on every login instead of reusing the stored grant will work for
its first hundred users and then start logging out the earliest ones.
Refresh tokens expire in seven days while the app is in testing. A Cloud
Console project whose publishing status is Testing issues refresh tokens that
die after a week. The symptom — works all week, invalid_grant on Monday — is
worth recognising before you go looking for a bug in your storage.
login_hint is a hint. It pre-fills the account chooser; the user can pick a
different account. If your product ties the grant to an email address, read the
address back from the API rather than trusting the one you asked for.
Where the rest is
- Getting the grant — the route, the session,
state, PKCE - Authorization servers — discovery, and what varies between servers
- Packs — what the six Google packs cover