Skip to content

Recover from stale cached OAuth access tokens without a browser - #2979

Merged
sfc-gh-turbaszek merged 5 commits into
mainfrom
oauth-code-silent-refresh-and-invalid-token-reauth
Aug 6, 2026
Merged

sfc-gh-turbaszek merged 5 commits into
mainfrom
oauth-code-silent-refresh-and-invalid-token-reauth

Conversation

@sfc-gh-turbaszek

Copy link
Copy Markdown
Contributor

Summary

OAuth authorization-code (and client-credentials) connections cache access/refresh tokens when client_store_temporary_credential=True. Two gaps made a cached token fail hard instead of recovering — the observable symptom was 250001 (08001) ... Invalid OAuth access token, only worked around by removing client_store_temporary_credential=True.

Gap A — proactive silent refresh in prepare()

prepare() only short-circuited on a cached access token. A fresh process holding only a valid refresh token went straight to the interactive browser flow. It now attempts a silent refresh with the cached refresh token before falling back to interactive auth. Gated on refresh_token_enabled, so behavior is unchanged when refresh tokens are off. The async path inherits this via the sync prepare().

Gap B — reauthenticate on invalid (not just expired) tokens

The connector only reauthenticated on GS code 390318 (access token expired). A token rejected as invalid — GS code 390303, "Invalid OAuth access token" (confirmed in gs_error_messages.properties) — fell through to a hard 250001 failure, and the poisoned token stayed cached so every subsequent run failed.

  • 390303 now triggers reauthentication in both sync and async auth paths, excluding OAuth v1.0 (AuthByOAuth), which cannot renew a user-supplied token.
  • reauthenticate() now evicts the stale access token from the cache on terminal failure via a lone remove() — never a remove-then-store, which would destroy the macOS Keychain ACL. On success, _store_tokens() overwrites the stale token ACL-safely.

Tests

  • Unit: proactive refresh (prepare()), invalid-token error handling (390303 -> ReauthenticationRequest), access-token eviction on reauth failure / no-eviction on success.
  • Wiremock integration: no-access-token -> silent refresh; 390303 -> refresh -> retry-login recovery.

All OAuth/auth suites green locally.

Notes for reviewers

  • Async prepare()/refresh runs blocking on the event loop — pre-existing (SNOW-2324426), not addressed here.
  • No JIRA ticket attached; happy to add one.

🤖 Generated with Claude Code

OAuth authorization-code (and client-credentials) connections cache the
access/refresh tokens when client_store_temporary_credential=True. Two gaps
made a cached token fail hard instead of recovering:

Gap A - prepare() only short-circuited on a cached access token; a fresh
process holding only a valid refresh token went straight to the browser. It
now attempts a silent refresh with the cached refresh token before falling
back to interactive auth (gated on refresh_token_enabled, so behavior is
unchanged when refresh tokens are off). Async inherits this via the sync
prepare().

Gap B - the connector only reauthenticated on GS code 390318 (access token
*expired*). A token rejected as *invalid* (390303, "Invalid OAuth access
token") fell through to a hard 250001 failure and the poisoned token stayed
cached, so every subsequent run failed until the credential cache was
disabled. 390303 now triggers reauthentication in both the sync and async
auth paths (excluding OAuth v1.0, which cannot renew a user-supplied token),
and reauthenticate() evicts the stale access token from the cache on terminal
failure via a lone remove() (never a remove-then-store, which would destroy
the macOS Keychain ACL).

Tests: unit coverage for proactive refresh, invalid-token error handling, and
access-token eviction; wiremock integration tests for the no-access-token
silent refresh and the 390303 -> refresh -> login recovery flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.95652% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.06%. Comparing base (c483115) to head (f8fd7d7).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/snowflake/connector/auth/_oauth_base.py 88.23% 0 Missing and 2 partials ⚠️
src/snowflake/connector/connection.py 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2979      +/-   ##
==========================================
+ Coverage   65.02%   65.06%   +0.04%     
==========================================
  Files         123      123              
  Lines       17410    17430      +20     
  Branches     2387     2392       +5     
==========================================
+ Hits        11321    11341      +20     
+ Misses       5649     5648       -1     
- Partials      440      441       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ve OAuth guard

Follow-up to the OAuth stale-token recovery work, addressing PR review:

- Remove the async-tier changes (aio/auth/_auth.py, aio/_connection.py). Async
  recovery is intentionally out of scope for this PR (NO_ASYNC_CHANGES).

- prepare(): wrap the proactive silent refresh so any failure - including a
  transient network error surfaced through _handle_failure as DatabaseError -
  degrades to interactive auth instead of hard-failing the connection. This
  differs intentionally from reauthenticate(), where raising mid-session is
  correct. Adds a unit test for the raising path.

- Reauth guard: replace the negative `not isinstance(auth_instance, AuthByOAuth)`
  with a positive `isinstance(auth_instance, AuthByOAuthBase)`. Both 390318
  (expired) and 390303 (invalid) are OAuth-scoped in GS, and only OAuth v2
  authenticators can renew a token; the positive check keeps OAuth v1.0 and any
  non-OAuth authenticator out of the reauth retry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sfc-gh-turbaszek
sfc-gh-turbaszek marked this pull request as ready for review August 3, 2026 15:06
@sfc-gh-turbaszek
sfc-gh-turbaszek requested a review from a team as a code owner August 3, 2026 15:06

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed ✅

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 4 of 9 files (5 tests skipped) · 5,352 lines reviewed · 5 candidates → 1 kept · retrieval: on

@sfc-gh-fpawlowski sfc-gh-fpawlowski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, left similar UD - related note


@pytest.mark.skipolddriver
@patch("snowflake.connector.auth._http_server.AuthHttpServer.DEFAULT_TIMEOUT", 30)
def test_oauth_code_invalid_access_token_triggers_refresh(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the same scenario tested / confirmed not-occurring in the UD?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UD backport label added to check in future

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed ✅

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 4 of 9 files (5 tests skipped) · 5,376 lines reviewed · 7 candidates → 0 kept · retrieval: on

@snowflake-security-bot snowflake-security-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snowflake Security Review

Security grade: A — Passed ✅

No security findings after adjudication. This PR passes the Snowflake Security Review.

📊 4 of 9 files (5 tests skipped) · 5,376 lines reviewed · 5 candidates → 1 kept · 1 duplicate collapsed · retrieval: on

…oken reauth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sfc-gh-turbaszek
sfc-gh-turbaszek merged commit 18a22b2 into main Aug 6, 2026
55 of 57 checks passed
@sfc-gh-turbaszek
sfc-gh-turbaszek deleted the oauth-code-silent-refresh-and-invalid-token-reauth branch August 6, 2026 07:56
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants