popit3/google-auth — Google OAuth2 WSGI Handler

WSGI script: ~/py/popit3/wsgi_google_oauth.py, deployed to /usr/local/www/wsgi-scripts/wsgi_google_oauth.py on gravlax. Runs the Google OAuth2 authorization code flow and writes the token to WebDAV. google_services.py fetches from WebDAV on RefreshError to self-heal.

See Re-authentication URLs for the renewal link.

Architecture

Runs as www-data (the web server user). No special daemon user. Secrets (client ID and secret) live in /etc/apache2/conf-available/wsgi-google-oauth.conf on gravlax — not in git, chmod 640 root:www-data.

Token is written to /var/www/webdav/google_tokens/google_token.json (owned admin:www-data, chmod 775). This is served via WebDAV at https://webdav.critchley.biz/google_tokens/google_token.json.

Flow

1. GET /authorize-google/ → script redirects browser to Google consent page, stores CSRF state in /tmp/google_auth_states/.

2. User approves scopes on Google's site.

3. Google redirects to https://www.critchley.biz/authorize-google/callback?code=...&state=....

4. Script validates state, exchanges code for token, writes to WebDAV. Shows success page.

5. On next popit3 cron run, google_services.py picks up the fresh token from WebDAV automatically.

Apache config (gravlax, not in git)

RedirectMatch ^/google$ /authorize-google/
WSGIScriptAlias /authorize-google /usr/local/www/wsgi-scripts/wsgi_google_oauth.py
<Location /authorize-google>
    SetEnv REDIRECT_URI https://www.critchley.biz/authorize-google/callback
    Include /etc/apache2/conf-available/wsgi-google-oauth.conf
</Location>
ProxyPassMatch ^/authorize-google/ !

wsgi-google-oauth.conf contains SetEnv GOOGLE_CLIENT_ID and SetEnv GOOGLE_CLIENT_SECRET. These are read from the WSGI per-request environ (NOT os.environ — see lessons below).

Critical: Apache SetEnv vs os.environ

SetEnv in a <Location> block populates the per-request WSGI environ dict passed to the application() function. It does NOT appear in os.environ. All helper functions that need config must accept and use the WSGI environ dict.

def _cfg(wsgi_environ, key, default=None):
    return wsgi_environ.get(key) or os.environ.get(key) or default

Critical: google-auth naive datetime

The google-auth library's _helpers.utcnow() returns a naive (timezone-unaware) datetime. If the token's expiry field is timezone-aware (e.g. 2026-06-25T13:10:49+00:00), comparing them raises TypeError: can't compare offset-naive and offset-aware datetimes.

Fix in google_services.py _dict_to_creds():

if expiry.tzinfo is not None:
    expiry = expiry.astimezone(timezone.utc).replace(tzinfo=None)

Google Cloud Console

Client: John Critchley experiments (1048362443290-2qr9pds7lvut0geoilo8pg0fdndqn575.apps.googleusercontent.com). Authorised redirect URI: https://www.critchley.biz/authorize-google/callback.

Scopes

https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/drive

Self-healing fallback in google_services.py

On RefreshError, _load_creds() fetches the token from WebDAV using .netrc credentials for webdav.critchley.biz, saves it to ~/.google_token.json, and retries. No manual intervention needed unless the WebDAV token is also expired.

Recurring risk: Testing-mode 7-day refresh token expiry

Incident 2026-07-08: the Google Calendar refresh token (renewed 2026-06-25) had silently expired, and had been failing on every single popit3 run for about two weeks ("Google refresh token invalid — checking WebDAV for fresh token..." followed by "Google Calendar unavailable: invalid_grant: Token has been expired or revoked." — the WebDAV fallback token was equally stale, so self-heal could not kick in). Non-fatal (popit3 exits RC 0 regardless — see popit3/ops), but David Lloyd bookings silently stopped syncing to Google Calendar for that whole period.

Likely root cause: if the Google Cloud OAuth consent screen for "John Critchley experiments" is still in Testing mode, Google caps refresh tokens at 7 days — June 25 → July 8 is suspiciously close to two 7-day cycles. Fix for renewal: visit https://www.critchley.biz/google (you will see "Google hasn't verified this app" — expected for a Testing-mode app, click through). Fix to stop recurrence: either publish the app out of Testing mode in Google Cloud Console, or confirm the account is added as a test user with a longer-lived grant. Not yet done as of 2026-07-08 — worth checking next time this expires.

Self-service renewal link in the DL Bookings error banner (added 2026-07-08)

MyDavidLloydSchedule.py's pipeline-error banner (see popit3/ops for the banner mechanism) now appends a "Renew Google auth" link straight to https://www.critchley.biz/google whenever an error message contains "Google" or "GCal". So next time a token expires, the David Lloyd Bookings report itself tells you what to click, instead of needing to look this page up.

version 2  ·  updated 2026-06-25