Summary
ToolHive currently implements token persistence and Dynamic Client Registration (DCR) persistence, but lacks support for tracking and renewing expired client secrets. When an OAuth provider issues a client secret with an expiration time (per RFC 7591), ToolHive should:
- Store the expiration time from the DCR response
- Monitor for secret expiration
- Automatically renew the client secret before it expires using the registration access token
Current Implementation Status
✅ Implemented
- Token Persistence: Refresh tokens are persisted with expiry tracking (
CachedTokenExpiry)
- DCR Client Credentials Persistence: Client ID and client secret are stored
- Client ID stored as plain text in
CachedClientID
- Client secret stored securely via secret manager reference in
CachedClientSecretRef
- Client Secret Expiry Storage: The
CachedSecretExpiry field in pkg/auth/remote/config.go stores the client secret expiration time
❌ Missing Implementation
-
DCR Response Metadata Not Fully Stored:
RegistrationAccessToken is not stored (needed for secret renewal)
RegistrationClientURI is not stored (needed for secret renewal)
-
Expiry Checking Logic:
- No logic to check if
CachedSecretExpiry indicates an expired or soon-to-expire secret
- No validation when restoring cached credentials
- Expired secrets are not detected before use
-
Renewal Logic:
- No implementation to renew client secret using RFC 7592 Update Client Registration
- No use of
RegistrationAccessToken and RegistrationClientURI for renewal
- No automatic renewal when secret is expiring soon
Technical Details
Relevant Code Locations
-
DCR Response Structure: pkg/auth/oauth/dynamic_registration.go
DynamicClientRegistrationResponse includes ClientSecretExpiresAt, RegistrationAccessToken, RegistrationClientURI
-
OAuth Flow Result: pkg/auth/discovery/discovery.go
OAuthFlowResult struct needs to include DCR metadata fields
-
Persistence Callback: pkg/auth/remote/handler.go
ClientCredentialsPersister type signature needs to accept additional parameters
wrapWithPersistence() calls the persister but doesn't pass expiry/metadata
-
Runner Implementation: pkg/runner/runner.go
SetClientCredentialsPersister() callback implementation needs to store expiry and registration metadata
-
Config Storage: pkg/auth/remote/config.go
CachedSecretExpiry field exists and is populated with expiry time
CachedRegTokenRef field exists but is never populated (needed for renewal)
RFC Compliance
RFC 7591 (OAuth 2.0 Dynamic Client Registration Protocol):
- Section 3.2.1: Defines
client_secret_expires_at in registration response - RFC 7591 Section 3.2.1
- Section 3.2.1: Defines
registration_access_token and registration_client_uri in registration response
RFC 7592 (OAuth 2.0 Dynamic Client Registration Management Protocol):
Proposed Implementation
Phase 1: Store Additional DCR Metadata
-
Extend OAuthFlowResult to include registration metadata:
type OAuthFlowResult struct {
// ... existing fields ...
// DCR metadata for secret renewal
RegistrationAccessToken string // For updating registration
RegistrationClientURI string // Endpoint for registration updates
}
-
Update handleDynamicRegistration():
- Store
RegistrationAccessToken and RegistrationClientURI from DCR response
- Pass these through to
OAuthFlowResult
-
Extend ClientCredentialsPersister signature:
type ClientCredentialsPersister func(
clientID string,
clientSecret string,
secretExpiry time.Time,
registrationAccessToken string,
registrationClientURI string,
) error
-
Update persistence in runner.go:
- Store
registrationAccessToken securely via secret manager in CachedRegTokenRef
- Store
registrationClientURI (can be plain text, or derive from issuer)
Phase 2: Expiry Checking
-
Add expiry check in resolveClientCredentials() (pkg/auth/remote/handler.go):
- Check if
CachedSecretExpiry indicates secret is expired or expiring soon (e.g., within 24 hours)
- If expired/expiring, trigger renewal before using credentials
-
Add validation in tryRestoreFromCachedTokens():
- Verify secret hasn't expired before attempting token refresh
- Return appropriate error if secret is expired
Phase 3: Secret Renewal
-
Implement RFC 7592 Update Client Registration:
- Create function to update client registration using
RegistrationAccessToken
- Use HTTP PUT request to
RegistrationClientURI per RFC 7592 Section 2.2
- Authenticate using
RegistrationAccessToken as Bearer token
- Include all client metadata fields in the request (as required by RFC 7592 Section 2.2)
- Handle response per RFC 7592 Section 2.1 with new
client_secret and updated client_secret_expires_at
- Response may include new
registration_access_token which must replace the old one
-
Automatic Renewal Logic:
- When secret is expiring soon, automatically renew it
- Update stored secret and expiry in config
- Handle renewal failures gracefully (log warning, allow manual intervention)
-
Error Handling:
- If renewal fails, log warning and continue with existing secret (may still work)
- If secret is already expired and renewal fails, return error requiring re-authentication
Testing Considerations
-
Unit Tests:
- Test expiry checking logic with various expiry times
- Test renewal flow with mock DCR server
- Test error handling for expired secrets
-
Integration Tests:
- Test with OAuth provider that issues expiring secrets
- Test automatic renewal before expiry
- Test behavior when renewal fails
-
Edge Cases:
- Zero expiry (secret never expires)
- Missing registration access token (some providers don't provide it)
- Clock skew handling
- Concurrent renewal attempts
Related Code References
- DCR Response:
pkg/auth/oauth/dynamic_registration.go:135-153
- OAuth Flow Result:
pkg/auth/discovery/discovery.go:505-517
- Persistence Callback:
pkg/auth/remote/handler.go:22-47
- Runner Persistence:
pkg/runner/runner.go:666-694
- Config Structure:
pkg/auth/remote/config.go:58-68
RFC References
Acceptance Criteria
Additional Notes
- This feature is important for long-running workloads that rely on DCR
- Some OAuth providers (e.g., Keycloak) issue client secrets with expiration times
- The implementation should be backward compatible with providers that don't expire secrets (zero expiry value)
Summary
ToolHive currently implements token persistence and Dynamic Client Registration (DCR) persistence, but lacks support for tracking and renewing expired client secrets. When an OAuth provider issues a client secret with an expiration time (per RFC 7591), ToolHive should:
Current Implementation Status
✅ Implemented
CachedTokenExpiry)CachedClientIDCachedClientSecretRefCachedSecretExpiryfield inpkg/auth/remote/config.gostores the client secret expiration time❌ Missing Implementation
DCR Response Metadata Not Fully Stored:
RegistrationAccessTokenis not stored (needed for secret renewal)RegistrationClientURIis not stored (needed for secret renewal)Expiry Checking Logic:
CachedSecretExpiryindicates an expired or soon-to-expire secretRenewal Logic:
RegistrationAccessTokenandRegistrationClientURIfor renewalTechnical Details
Relevant Code Locations
DCR Response Structure:
pkg/auth/oauth/dynamic_registration.goDynamicClientRegistrationResponseincludesClientSecretExpiresAt,RegistrationAccessToken,RegistrationClientURIOAuth Flow Result:
pkg/auth/discovery/discovery.goOAuthFlowResultstruct needs to include DCR metadata fieldsPersistence Callback:
pkg/auth/remote/handler.goClientCredentialsPersistertype signature needs to accept additional parameterswrapWithPersistence()calls the persister but doesn't pass expiry/metadataRunner Implementation:
pkg/runner/runner.goSetClientCredentialsPersister()callback implementation needs to store expiry and registration metadataConfig Storage:
pkg/auth/remote/config.goCachedSecretExpiryfield exists and is populated with expiry timeCachedRegTokenReffield exists but is never populated (needed for renewal)RFC Compliance
RFC 7591 (OAuth 2.0 Dynamic Client Registration Protocol):
client_secret_expires_atin registration response - RFC 7591 Section 3.2.1registration_access_tokenandregistration_client_uriin registration responseRFC 7592 (OAuth 2.0 Dynamic Client Registration Management Protocol):
registration_client_uri- RFC 7592 Section 2.2Proposed Implementation
Phase 1: Store Additional DCR Metadata
Extend
OAuthFlowResultto include registration metadata:Update
handleDynamicRegistration():RegistrationAccessTokenandRegistrationClientURIfrom DCR responseOAuthFlowResultExtend
ClientCredentialsPersistersignature:Update persistence in
runner.go:registrationAccessTokensecurely via secret manager inCachedRegTokenRefregistrationClientURI(can be plain text, or derive from issuer)Phase 2: Expiry Checking
Add expiry check in
resolveClientCredentials()(pkg/auth/remote/handler.go):CachedSecretExpiryindicates secret is expired or expiring soon (e.g., within 24 hours)Add validation in
tryRestoreFromCachedTokens():Phase 3: Secret Renewal
Implement RFC 7592 Update Client Registration:
RegistrationAccessTokenRegistrationClientURIper RFC 7592 Section 2.2RegistrationAccessTokenas Bearer tokenclient_secretand updatedclient_secret_expires_atregistration_access_tokenwhich must replace the old oneAutomatic Renewal Logic:
Error Handling:
Testing Considerations
Unit Tests:
Integration Tests:
Edge Cases:
Related Code References
pkg/auth/oauth/dynamic_registration.go:135-153pkg/auth/discovery/discovery.go:505-517pkg/auth/remote/handler.go:22-47pkg/runner/runner.go:666-694pkg/auth/remote/config.go:58-68RFC References
client_secret_expires_at,registration_access_token,registration_client_uri)Acceptance Criteria
RegistrationAccessToken,RegistrationClientURI) is storedAdditional Notes