Replaces the _zero_refresh_expiry_means_never_expires class attribute with
a proper override hook. The base class stays free of any provider-specific
knowledge; KeycloakOAuthProxy overrides the method and returns True for val==0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
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>
The initial auth-code exchange path had a bug where val==0 kept
refresh_expires_in as None, causing the fallback to be applied
despite the Keycloak never-expires sentinel. Now uses the same
keycloak_never_expires flag pattern as the other two code paths.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix OCI Provider issue in 3.x version. Add OCI auth provider example and test
* Fix OCI Provider issue in 3.x version. Add OCI auth provider example and test. Fixed a couple of minor issues in README.
* Rerun CI