feat: Add client secret expiry tracking and automatic renew for DCR#4194
feat: Add client secret expiry tracking and automatic renew for DCR#4194Sanskarzz wants to merge 8 commits intostacklok:mainfrom
Conversation
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4194 +/- ##
==========================================
- Coverage 68.77% 68.66% -0.11%
==========================================
Files 473 474 +1
Lines 47919 48115 +196
==========================================
+ Hits 32955 33040 +85
- Misses 12299 12342 +43
- Partials 2665 2733 +68 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
294c25f to
a104b56
Compare
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
PR size has been reduced below the XL threshold. Thank you for splitting this up!
|
✅ PR size has been reduced below the XL threshold. The size review has been dismissed and this PR can now proceed with normal review. Thank you for splitting this up! |
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
Summary
Implements client secret expiry tracking and automatic renewal for Dynamic Client Registration (DCR)
Fixes: #3631
ToolHive already stored the
client_idandclient_secretfrom DCR responses, but discarded theregistration_access_token,registration_client_uri, andclient_secret_expires_atfields. Without these, there was no way to detect or renew an expired client secret, causing silent authentication failures for long-running workloads on providers like Keycloak that issue expiring secrets.This PR implements the full RFC 7591 / RFC 7592 secret lifecycle in three phases:
Note
All the phases are added in the same PR.
Changes
pkg/auth/remote/persisting_token_source.goExtended
ClientCredentialsPersisterfunction type signature:pkg/auth/remote/config.goCachedRegClientURIfield to storeregistration_client_urias plain textCachedTokenEndpointAuthMethodfield to store the authentication method used during DCRClearCachedClientCredentials()to also clear the new fieldspkg/auth/discovery/discovery.goOAuthFlowConfig(used as the threading vehicle fromhandleDynamicRegistrationthrough to the result)OAuthFlowResultwithSecretExpiry,RegistrationAccessToken,RegistrationClientURI, andTokenEndpointAuthMethodhandleDynamicRegistration()to capture all four fromDynamicClientRegistrationResponse:ClientSecretExpiresAt > 0→time.Unix(...)(zero if the field is0, meaning never expires)RegistrationAccessTokenandRegistrationClientURIcopied as-isTokenEndpointAuthMethodcaptured from response (defaults toclient_secret_postin request)newOAuthFlow()to populate the new fields inOAuthFlowResultpkg/auth/remote/handler.gowrapWithPersistence()to pass all 6 arguments toclientCredentialsPersister"time"importresolveClientCredentials()now proactively callsrenewClientSecret()when the secret is expiring within 24 h; renewal failures are soft-logged and execution continuestryRestoreFromCachedTokens()now checks expiry before attempting token refresh:pkg/runner/runner.goUpdated
SetClientCredentialsPersistercallback to match the new 6-argument signature and persist:CachedSecretExpiry— stored directly in configregistrationAccessToken— stored securely in the secret manager, reference saved toCachedRegTokenRefregistrationClientURI— stored as plain text inCachedRegClientURItokenEndpointAuthMethod— stored directly in config asCachedTokenEndpointAuthMethodpkg/auth/remote/secret_renewal.go(new file)Implements RFC 7592 §2.2 client secret renewal:
isSecretExpiredOrExpiringSoon()— returns true when the secret is withinsecretExpiryBuffer(24 h) of expiry or already past it; false for zero expiry (never expires)renewClientSecret(ctx)— sends an authenticated HTTPPUTtoregistration_client_uriper RFC 7592 §2.2:registrationAccessTokenfrom the secret managerregistration_client_uri(must be HTTPS or localhost)client_secret,client_secret_expires_at, and optionally a rotatedregistration_access_tokenclientCredentialsPersistervalidateRegistrationClientURI(uri)— validates the URI is HTTPS (or localhost for development)pkg/auth/remote/secret_renewal_test.go(new file)16 new tests covering:
TestIsSecretExpiredOrExpiringSoon(5 cases)TestValidateRegistrationClientURI(6 cases)TestRenewClientSecret_MissingConfig(3 cases)TestRenewClientSecret_SuccessTestRenewClientSecret_ServerErrorTestRenewClientSecret_NoPersisterTestRenewClientSecret_ZeroExpiryInResponseclient_secret_expires_at: 0→time.Time{}(never expires)Breaking Changes
Warning
ClientCredentialsPersisterfunction type signature changed.Any code outside this repository that implements or calls
ClientCredentialsPersistermust be updated to use the new 6-parameter signature. Within this repository, the only call site ispkg/runner/runner.go, which is updated in this PR.Backward Compatibility
client_secret_expires_at = 0)CachedSecretExpiryistime.Time{}.isSecretExpiredOrExpiringSoon()returnsfalse. No renewal is attempted.registration_access_token/ URI)renewClientSecret()returns an immediate error. Caller logs a warning and continues with the existing secret.RFC References
client_secret_expires_at,registration_access_token,registration_client_uriin registration responseTest Results
Type of change
Test plan
I have done E2E testing with Keycloak.