Commit graph

24 commits

Author SHA1 Message Date
strawgate
2e1178fa84 fix: forward resource_base_url and issuer_url through KeycloakOAuthProxy
🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:05:29 -05:00
strawgate
f19df2dce7 fix: forward redirect_path through KeycloakOAuthProxy for custom callbacks
🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 12:11:59 -05:00
strawgate
9402d37e1b test+docs: transparent-refresh sentinel coverage and KeycloakOAuthProxy docs
🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 00:26:17 -05:00
strawgate
bfd38fc45a feat: forward valid_scopes through KeycloakOAuthProxy for offline_access DCR
🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:49:06 -05:00
strawgate
97649771d1 feat: add KeycloakOAuthProxy with offline token support
Keycloak returns refresh_expires_in=0 for offline_access tokens to signal
'this refresh token never expires'. Base OAuthProxy has no way to know this
is intentional rather than a malformed response, so it falls through to the
standard 1-year wall-clock fallback — which causes the FastMCP refresh token
TTL to shrink on every subsequent refresh cycle until it reaches ~0 after
one year, forcing re-authentication even though the Keycloak offline token
is still valid.

This commit:
- Adds KeycloakOAuthProxy(OAuthProxy) to providers/keycloak.py with a
  convenience __init__ that derives OIDC endpoints from realm_url
- Adds _zero_refresh_expiry_means_never_expires: bool = False class attr
  on OAuthProxy; KeycloakOAuthProxy sets it to True
- Adds refresh_token_never_expires: bool = False to UpstreamTokenSet so
  the intent is visible in stored state
- When the flag is set and val==0: marks the token as never-expiring and
  clears refresh_token_expires_at so subsequent refresh cycles always get
  a fresh full fallback-TTL FastMCP RT instead of a decaying one
- Base OAuthProxy is completely unchanged for val==0: falls through to
  the existing 1-year wall-clock fallback as before

Tests:
- test_refresh_expires_in_zero_issues_refresh_token: KeycloakOAuthProxy
  correctly issues a refresh token and marks upstream as never-expiring
- test_refresh_expires_in_zero_subsequent_refresh_does_not_shrink: TTL
  stays at ~1 year after repeated refresh cycles
- test_base_proxy_does_not_treat_zero_as_never_expires: confirms base
  OAuthProxy behaviour is unaffected

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-13 15:30:15 -05:00
strawgate
08baf0763b fix: clear stale refresh_token_expires_at for Keycloak offline tokens on subsequent refreshes
When Keycloak returns refresh_expires_in=0 (offline token, never expires) on
every token response, the previous code only guarded the initial exchange path.
On subsequent exchange_refresh_token and transparent refresh cycles, the code
would fall through to 'keep existing expiry' — inheriting the wall-clock
timestamp set at initial exchange.  After ~1 year that decayed to ~0 seconds,
issuing FastMCP RTs with 1-second TTL and forcing re-auth even though the
Keycloak offline token was still valid.

Fix: add elif val == 0 to both refresh paths that clears refresh_token_expires_at
to None.  The fallback branch then issues a fresh full fallback-TTL FastMCP RT
on every cycle, matching the 'always-valid' semantics of offline tokens.

Also improves the debug log message to distinguish 'never expires' from
'expiry not provided' so operators can see exactly what Keycloak sent.

Adds a regression test: test_refresh_expires_in_zero_subsequent_refresh_does_not_shrink

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-13 12:38:50 -05:00
Taylor Wilsdon
61e56c683e
enh: Add public API for updating OAuthProxy scopes after initialization (#4091) 2026-05-04 15:49:12 -04:00
Jeremiah Lowin
2d6143c6d8
fix: honor upstream refresh token expiry in OAuthProxy (#3990) 2026-04-20 14:03:57 -04:00
Jeremiah Lowin
801385df44
fix: bound _refresh_locks with LRU eviction to prevent memory leak (#3968) 2026-04-18 11:07:38 -04:00
Jeremiah Lowin
eb1426251c
Allow upstream client_id to be used directly without DCR (#3957)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 15:22:44 -04:00
Adam Azzam
f21e51794c
Allow auth providers to override protected resource base URLs (#3900)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-13 13:36:55 -04:00
Jeremiah Lowin
1d39e26025
Fix CIMD clients getting required_scopes instead of valid_scopes (#3836) 2026-04-12 16:34:56 -04:00
Fatia Kusuma Dewi
5587cb7c43
changeable allowed_client_redirect_uris (#3772) 2026-04-06 20:58:09 -04:00
Kakarlamudi Venkata Dhanush
c642d6a5cf
fix: propagate upstream_claims in load_access_token (#3750) 2026-04-06 19:55:26 -04:00
Jeremiah Lowin
c397e68d39
Update ty ignore comments for 0.0.25 compatibility (#3614) 2026-03-24 20:26:26 -04:00
Jeremiah Lowin
52feff6878
Transparently refresh upstream token in OAuthProxy.load_access_token() (#3584)
* Transparently refresh upstream token in OAuthProxy.load_access_token()

When upstream token validation fails during load_access_token, attempt
to refresh using the stored refresh token before returning None. This
prevents premature 401s that force clients into expensive full re-auth
flows when the upstream token expires.

Co-authored-by: Claude <noreply@anthropic.com>

* Gate transparent refresh on token expiry, add advisory lock

Only attempt upstream refresh when the token is actually expired, not
on any validation failure (scope mismatch, revocation, etc.). Add
per-token advisory lock to prevent concurrent async tasks from racing
to refresh the same upstream token.

* Re-check expiry inside lock, reload from storage after refresh failure

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-22 10:43:44 -04:00
Jeremiah Lowin
bb37c3bb6b
fix: reject refresh tokens used as Bearer access tokens (#3524) 2026-03-15 15:21:30 -04:00
Jeremiah Lowin
32dfe50f39
Treat refresh_expires_in=0 as missing, fall back to 30-day default (#3514)
Keycloak returns refresh_expires_in=0 for offline tokens (offline_access scope),
meaning "no fixed time-based expiry". The truthiness check on this value caused
the proxy to skip issuing a PROXY_RT, forcing browser re-auth every hour.

Closes #3509

🤖 Generated with Claude Code

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-15 11:49:50 -04:00
Jeremiah Lowin
ea529f6a49
feat: make upstream_client_secret optional in OAuthProxy (#3486)
* feat: make upstream_client_secret optional in OAuthProxy

Extract _create_upstream_oauth_client() factory method for subclass
override. Cookie signing falls back to JWT key material when no secret.

* fix: include client_id in revocation requests for public clients

* fix: use factory method for revocation auth
2026-03-15 11:14:56 -04:00
Jeremiah Lowin
799c4f1673
Escape client_id in OAuth consent details (#3418)
🤖 Generated with GPT-5.2-Codex
2026-03-06 17:33:12 -05:00
Jeremiah Lowin
880d835ccc
Add CIMD (Client ID Metadata Document) support for OAuth (#2871) 2026-02-06 13:44:52 -05:00
claude[bot]
cec40b378d Use MemoryStore for OAuth proxy tests
Updated all OAuthProxy test instantiations to use MemoryStore instead of defaulting to DiskStore, avoiding SQLite timeout issues on Windows and improving test performance.

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-02-01 02:30:05 +00:00
Abhijeeth Padarthi
cee99d1210
Normalize resource URLs before comparison to support RFC 8707 query parameters (#2967)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-01-22 19:41:02 -05:00
Jeremiah Lowin
23bfdf0680
Consolidate test fixtures and refactor large test files (#2941) 2026-01-19 15:18:35 -05:00