77 "context"
88 "fmt"
99 "log/slog"
10+ "time"
1011
1112 "golang.org/x/oauth2"
1213
@@ -187,7 +188,13 @@ func (h *Handler) wrapWithPersistence(result *discovery.OAuthFlowResult) oauth2.
187188 // Persist DCR client credentials if available (for servers that use Dynamic Client Registration)
188189 // Only persist if client_id exists - client_secret may be empty for PKCE flows
189190 if h .clientCredentialsPersister != nil && result .ClientID != "" {
190- if err := h .clientCredentialsPersister (result .ClientID , result .ClientSecret ); err != nil {
191+ if err := h .clientCredentialsPersister (
192+ result .ClientID ,
193+ result .ClientSecret ,
194+ result .SecretExpiry ,
195+ result .RegistrationAccessToken ,
196+ result .RegistrationClientURI ,
197+ ); err != nil {
191198 slog .Warn ("Failed to persist DCR client credentials" , "error" , err )
192199 } else {
193200 slog .Debug ("Successfully persisted DCR client credentials for future restarts" )
@@ -205,6 +212,8 @@ func (h *Handler) wrapWithPersistence(result *discovery.OAuthFlowResult) oauth2.
205212
206213// resolveClientCredentials returns the client ID and secret to use, preferring
207214// cached DCR credentials over statically configured ones.
215+ // If the cached client secret is expiring soon, it attempts renewal via RFC 7592
216+ // before returning the credentials.
208217func (h * Handler ) resolveClientCredentials (ctx context.Context ) (clientID , clientSecret string ) {
209218 // First try to use statically configured credentials
210219 clientID = h .config .ClientID
@@ -216,6 +225,18 @@ func (h *Handler) resolveClientCredentials(ctx context.Context) (clientID, clien
216225 clientID = h .config .CachedClientID
217226 slog .Debug ("Using cached DCR client credentials" , "client_id" , clientID )
218227
228+ // Proactively renew the client secret if it is expiring soon (RFC 7592)
229+ if h .isSecretExpiredOrExpiringSoon () {
230+ slog .Info ("Cached client secret is expiring soon, attempting renewal" ,
231+ "expiry" , h .config .CachedSecretExpiry )
232+ if renewErr := h .renewClientSecret (ctx ); renewErr != nil {
233+ slog .Warn ("Failed to proactively renew client secret; continuing with existing secret" ,
234+ "error" , renewErr )
235+ } else {
236+ slog .Debug ("Successfully renewed client secret ahead of expiry" )
237+ }
238+ }
239+
219240 // Client secret is stored securely and may be empty for PKCE flows
220241 if h .config .CachedClientSecretRef != "" && h .secretProvider != nil {
221242 cachedClientSecret , err := h .secretProvider .GetSecret (ctx , h .config .CachedClientSecretRef )
@@ -242,6 +263,27 @@ func (h *Handler) tryRestoreFromCachedTokens(
242263 return nil , fmt .Errorf ("secret provider not configured, cannot restore cached tokens" )
243264 }
244265
266+ // Check if the cached client secret is expired before attempting token refresh.
267+ // If it has fully expired and renewal also fails we must force a fresh OAuth flow.
268+ if h .isSecretExpiredOrExpiringSoon () {
269+ slog .Info ("Cached client secret is expiring or expired; attempting renewal before token restore" ,
270+ "expiry" , h .config .CachedSecretExpiry )
271+ if renewErr := h .renewClientSecret (ctx ); renewErr != nil {
272+ slog .Warn ("Client secret renewal failed" , "error" , renewErr )
273+ // Hard-fail only when the secret is already past its expiry.
274+ // If we are still in the buffer window the existing secret may work.
275+ if ! h .config .CachedSecretExpiry .IsZero () && time .Now ().After (h .config .CachedSecretExpiry ) {
276+ return nil , fmt .Errorf (
277+ "client secret expired at %v and renewal failed: %w" ,
278+ h .config .CachedSecretExpiry , renewErr )
279+ }
280+ // Still within buffer — log and continue with the existing (still-valid) secret
281+ slog .Warn ("Proceeding with expiring client secret after failed renewal attempt" )
282+ } else {
283+ slog .Debug ("Successfully renewed client secret before token restore" )
284+ }
285+ }
286+
245287 refreshToken , err := h .secretProvider .GetSecret (ctx , h .config .CachedRefreshTokenRef )
246288 if err != nil {
247289 return nil , fmt .Errorf ("failed to retrieve cached refresh token: %w" , err )
0 commit comments