popit3/outlook-token-renewal — Outlook OAuth Token Renewal Flow

The Outlook POP3 refresh token expires periodically. This documents the full renewal flow introduced June 2026, replacing the old redirect-based WSGI script (msauth).

How it Works

Token renewal uses Microsoft device flow (same as the CLI script get_pop_refresh_token.py). No redirect URI registration is required.

1. Renew the Token (Browser)

Visit: https://www.critchley.biz/outlook

The page (served by wsgi_outlook_oauth.py on gravlax) will:

1. Show a short code in a large box (click to copy)

2. Offer a button to open Microsoft Device Login in a popup

3. Poll every 2 seconds in the background

4. When auth completes, show confirmation and save token to WebDAV

2. Token Storage (WebDAV on gravlax)

Token is saved to: /var/www/webdav/outlook_tokens/jsr_critchley_at_hotmail_com.json

File contains: token_type, scope, expires_in, access_token, refresh_token, user_email, saved_at

3. Auto-Recovery in popit3.py

When popit3.py runs on pomelo and the refresh token fails (expired grant), it:

1. Catches the RuntimeError from acquire_access_token_via_refresh

2. Calls fetch_refresh_token_from_webdav(user_email) — HTTP GET to https://webdav.critchley.biz/outlook_tokens/jsr_critchley_at_hotmail_com.json with Basic Auth from ~/.netrc (machine webdav.critchley.biz)

3. Calls update_netrc_refresh_token(machine, new_token) — rewrites the password field for outlook.office365.com in ~/.netrc

4. Retries acquire_access_token_via_refresh with the new token and continues normally

Components

wsgi_outlook_oauth.py (gravlax)

Deployed to: /usr/local/www/wsgi-scripts/wsgi_outlook_oauth.py

Apache config: WSGIScriptAlias /authorize-outlook /usr/local/www/wsgi-scripts/wsgi_outlook_oauth.py

Short URL redirect: RedirectMatch ^/outlook$ /authorize-outlook/authorize

Routes:

/authorize-outlook/authorize — initiate device flow, show code + button

/authorize-outlook/status?flow_id=X — single-shot poll (non-blocking POST to MS token endpoint)

/authorize-outlook/list — show stored tokens

Flow state stored in /tmp/outlook_wsgi_flows/{sha256_of_device_code[:16]}.json (disk, not memory, so all Apache worker processes can access it)

popit3.py (pomelo)

Functions added:

fetch_refresh_token_from_webdav(user_email) — fetches JSON from WebDAV, returns refresh_token field

update_netrc_refresh_token(machine, new_token) — updates password in ~/.netrc, preserves all other fields, sets permissions 600

Fallback only triggers when refresh token exchange raises RuntimeError (not on cached access token expiry).

netrc Format

machine outlook.office365.com
  login jsr_critchley@hotmail.com
  account MSAL:60da67f7-5fde-4e85-baf3-ab28d0c8e034
  password <refresh_token>

machine webdav.critchley.biz
  login john
  password <webdav_password>

Caching

Access tokens are cached in ~/.outlook.office365.com_token (JSON, valid for ~3600s). The refresh token fallback only runs when this cache is absent or expired AND the refresh token exchange fails.

Azure App Registration

App: Popit2 (60da67f7-5fde-4e85-baf3-ab28d0c8e034). Device flow requires Allow public client flows enabled. No redirect URI registration needed for device flow.

Troubleshooting: blank/white popup

If the Microsoft device-login popup opens but stays blank (stuck on "Transferring data from login.live.com..."), check for Firefox Enhanced Tracking Protection or a similar ad/tracker-blocking extension. Confirmed 2026-07-08: ETP was silently blocking requests to logincdn.msauth.net (the CSS/JS the login page needs to render), leaving the page permanently blank.

Fix: click the shield icon in the Firefox address bar and turn off Enhanced Tracking Protection for login.live.com (or whitelist logincdn.msauth.net in the blocking extension).

Diagnose via Firefox DevTools > Network tab: blocked requests show status 0 with no timing data at all (blocked client-side before hitting the network), while the main login.live.com document itself loads fine (200 OK).