From ef448100e647ca10ffce891916493e50b9b4f57e Mon Sep 17 00:00:00 2001 From: Zsolt Rappi Date: Sat, 21 Mar 2026 15:52:08 +0100 Subject: [PATCH 1/4] feat: handle re-auth flows --- pkg/oauth2ac/oauth2ac.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pkg/oauth2ac/oauth2ac.go b/pkg/oauth2ac/oauth2ac.go index bb41ce6..ba4a907 100644 --- a/pkg/oauth2ac/oauth2ac.go +++ b/pkg/oauth2ac/oauth2ac.go @@ -270,10 +270,6 @@ func (cp *credentialsProvider) Start(ctx context.Context) (<-chan StatusEvent, e } func (cp *credentialsProvider) Authorize(ctx context.Context, authState, code string) (*oauth2.Token, error) { - if cp.syncGate.IsOpen() { - return nil, errors.WithStack(ErrAlreadyAuthorized) - } - token, err := cp.exchangeToken(ctx, authState, code) if err != nil { return nil, err @@ -291,6 +287,10 @@ func (cp *credentialsProvider) Authorize(ctx context.Context, authState, code st return nil, err } + if cp.syncGate.IsOpen() { + cp.signalRefresh() + } + return token, nil } @@ -547,24 +547,30 @@ func (cp *credentialsProvider) tokenRefresherLoop(ctx context.Context) { select { case <-cp.refreshCh: - cp.logger.Info("client credentials changed: reset init condition and wait for re-authorization") - cp.mu.RLock() serr := cp.secretError cp.mu.RUnlock() - err := errors.WithStack(ErrAuthorizationNeeded) if serr != nil { - err = errors.Wrap(err, serr.Error()) + cp.logger.Info("client credentials changed: reset init condition and wait for re-authorization") + + err := errors.Wrap(errors.WithStack(ErrAuthorizationNeeded), serr.Error()) + cp.publishCredential(Credential{ + Event: credential.RemoveEventType, + Err: err, + }) + + cp.setUnauthorizedStatus(err) + + continue } + + cp.logger.Info("re-authorization: replacing token") cp.publishCredential(Credential{ Event: credential.RemoveEventType, - Err: err, + Err: errors.WithStack(ErrAuthorizationNeeded), }) - - cp.setUnauthorizedStatus(err) - - continue + refreshTime = 0 case <-ctx.Done(): return case <-time.After(refreshTime): From fdec1f48423f8c589446f5fb6d313b9b62ecd70c Mon Sep 17 00:00:00 2001 From: Zsolt Rappi Date: Tue, 7 Apr 2026 16:27:23 +0200 Subject: [PATCH 2/4] chore: make oauth2ac ttl configurable --- pkg/oauth2ac/oauth2ac.go | 27 ++++++++++++++------ pkg/oauth2ac/options.go | 54 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 pkg/oauth2ac/options.go diff --git a/pkg/oauth2ac/oauth2ac.go b/pkg/oauth2ac/oauth2ac.go index ba4a907..3b229fa 100644 --- a/pkg/oauth2ac/oauth2ac.go +++ b/pkg/oauth2ac/oauth2ac.go @@ -155,8 +155,10 @@ type credentialsProvider struct { clientSecret string secretError error - statesMu sync.RWMutex - authStates map[string]authState + statesMu sync.RWMutex + authStates map[string]authState + authStateTTL time.Duration + authStateCleanupInterval time.Duration syncGate *util.SyncGate @@ -190,12 +192,12 @@ type TokenStorage interface { // - logger: Logger for logging credential operations // // Returns a credential provider and an error if creation fails -func NewCredentialsProvider(id string, cache cache.Cache, tokenStore TokenStorage, cfg *CredentialsConfig, logger logr.Logger) (*credentialsProvider, error) { +func NewCredentialsProvider(id string, cache cache.Cache, tokenStore TokenStorage, cfg *CredentialsConfig, logger logr.Logger, opts ...option.Option) (*credentialsProvider, error) { if err := validateConfig(cfg); err != nil { return nil, err } - return &credentialsProvider{ + cp := &credentialsProvider{ id: id, cache: cache, tokenStore: tokenStore, @@ -209,7 +211,18 @@ func NewCredentialsProvider(id string, cache cache.Cache, tokenStore TokenStorag authStates: map[string]authState{}, syncGate: util.NewSyncGate(), pipe: eventbus.NewPipe[Credential](), - }, nil + + authStateTTL: time.Minute * 5, + authStateCleanupInterval: time.Second * 5, + } + + for _, o := range opts { + if o, ok := isCredentialsProviderOption(o); ok { + o.Apply(cp) + } + } + + return cp, nil } func (cp *credentialsProvider) ID() string { @@ -245,7 +258,7 @@ func (cp *credentialsProvider) Start(ctx context.Context) (<-chan StatusEvent, e cp.running.Store(true) // periodic cleanup of expired states - go util.RunFuncAtInterval(ctx, time.Second*5, func(_ context.Context) error { + go util.RunFuncAtInterval(ctx, cp.authStateCleanupInterval, func(_ context.Context) error { cp.cleanupExpiredStates() return nil @@ -742,7 +755,7 @@ func (cp *credentialsProvider) cleanupExpiredStates() { cp.statesMu.RLock() for k, authState := range cp.authStates { - if authState.issuedAt.Before(time.Now().Add(-time.Second * 60)) { + if authState.issuedAt.Before(time.Now().Add(-cp.authStateTTL)) { candidates = append(candidates, k) } } diff --git a/pkg/oauth2ac/options.go b/pkg/oauth2ac/options.go new file mode 100644 index 0000000..9648aad --- /dev/null +++ b/pkg/oauth2ac/options.go @@ -0,0 +1,54 @@ +// Copyright (c) 2025 Riptides Labs, Inc. +// SPDX-License-Identifier: MIT + +package oauth2ac + +import ( + "time" + + "go.riptides.io/tokenex/pkg/option" +) + +// CredentialsProviderOption is a function that modifies the credentialsProvider. +type ( + CredentialsProviderOption interface { + Apply(*credentialsProvider) + } + credentialsProviderOption struct { + option.Option + + f func(*credentialsProvider) + } +) + +func (o *credentialsProviderOption) Apply(cp *credentialsProvider) { + o.f(cp) +} + +func withCredentialsProviderOption(f func(*credentialsProvider)) option.Option { + return &credentialsProviderOption{option.OptionImpl{}, f} +} + +func isCredentialsProviderOption(opt any) (CredentialsProviderOption, bool) { + if o, ok := opt.(*credentialsProviderOption); ok { + return o, ok + } + + return nil, false +} + +// WithAuthStateTTL sets how long an auth state is kept before being considered expired. +// Defaults to 5 minutes. +func WithAuthStateTTL(ttl time.Duration) option.Option { + return withCredentialsProviderOption(func(cp *credentialsProvider) { + cp.authStateTTL = ttl + }) +} + +// WithAuthStateCleanupInterval sets how often expired auth states are swept. +// Defaults to 5 seconds. +func WithAuthStateCleanupInterval(d time.Duration) option.Option { + return withCredentialsProviderOption(func(cp *credentialsProvider) { + cp.authStateCleanupInterval = d + }) +} From a5112835be12d1ade8f9bc0fbe8111d3e7e08be5 Mon Sep 17 00:00:00 2001 From: Zsolt Rappi Date: Tue, 7 Apr 2026 17:15:35 +0200 Subject: [PATCH 3/4] chore: make oauth2 re-auth configurable --- pkg/oauth2ac/oauth2ac.go | 6 ++++++ pkg/oauth2ac/options.go | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/oauth2ac/oauth2ac.go b/pkg/oauth2ac/oauth2ac.go index 3b229fa..521a736 100644 --- a/pkg/oauth2ac/oauth2ac.go +++ b/pkg/oauth2ac/oauth2ac.go @@ -159,6 +159,7 @@ type credentialsProvider struct { authStates map[string]authState authStateTTL time.Duration authStateCleanupInterval time.Duration + reauthorizeIfAuthorized bool syncGate *util.SyncGate @@ -214,6 +215,7 @@ func NewCredentialsProvider(id string, cache cache.Cache, tokenStore TokenStorag authStateTTL: time.Minute * 5, authStateCleanupInterval: time.Second * 5, + reauthorizeIfAuthorized: true, } for _, o := range opts { @@ -283,6 +285,10 @@ func (cp *credentialsProvider) Start(ctx context.Context) (<-chan StatusEvent, e } func (cp *credentialsProvider) Authorize(ctx context.Context, authState, code string) (*oauth2.Token, error) { + if !cp.reauthorizeIfAuthorized && cp.syncGate.IsOpen() { + return nil, errors.WithStack(ErrAlreadyAuthorized) + } + token, err := cp.exchangeToken(ctx, authState, code) if err != nil { return nil, err diff --git a/pkg/oauth2ac/options.go b/pkg/oauth2ac/options.go index 9648aad..030fcaf 100644 --- a/pkg/oauth2ac/options.go +++ b/pkg/oauth2ac/options.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Riptides Labs, Inc. +// Copyright (c) 2026 Riptides Labs, Inc. // SPDX-License-Identifier: MIT package oauth2ac @@ -37,6 +37,15 @@ func isCredentialsProviderOption(opt any) (CredentialsProviderOption, bool) { return nil, false } +// WithReauthorizeIfAuthorized controls whether completing a new auth flow signals a token +// refresh when the provider is already authorized (i.e. has a valid token). +// Defaults to true. +func WithReauthorizeIfAuthorized(v bool) option.Option { + return withCredentialsProviderOption(func(cp *credentialsProvider) { + cp.reauthorizeIfAuthorized = v + }) +} + // WithAuthStateTTL sets how long an auth state is kept before being considered expired. // Defaults to 5 minutes. func WithAuthStateTTL(ttl time.Duration) option.Option { From 87439ea29ec8a91613bc479942744f838e77e7e5 Mon Sep 17 00:00:00 2001 From: Zsolt Rappi Date: Tue, 7 Apr 2026 17:23:40 +0200 Subject: [PATCH 4/4] fix: linter --- pkg/oauth2ac/oauth2ac.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/oauth2ac/oauth2ac.go b/pkg/oauth2ac/oauth2ac.go index 521a736..39b3cfc 100644 --- a/pkg/oauth2ac/oauth2ac.go +++ b/pkg/oauth2ac/oauth2ac.go @@ -248,9 +248,7 @@ func (cp *credentialsProvider) Start(ctx context.Context) (<-chan StatusEvent, e return nil, errors.WithStack(ErrAlreadyRunning) } - if err := cp.authorizeIfTokenExists(ctx); err != nil { - return nil, err - } + cp.authorizeIfTokenExists(ctx) handlerStopFunc, err := cp.startInformer(ctx) if err != nil { @@ -260,7 +258,7 @@ func (cp *credentialsProvider) Start(ctx context.Context) (<-chan StatusEvent, e cp.running.Store(true) // periodic cleanup of expired states - go util.RunFuncAtInterval(ctx, cp.authStateCleanupInterval, func(_ context.Context) error { + go util.RunFuncAtInterval(ctx, cp.authStateCleanupInterval, func(_ context.Context) error { //nolint:unparam cp.cleanupExpiredStates() return nil @@ -428,7 +426,7 @@ func (cp *credentialsProvider) storeTokenAndAuthorize(ctx context.Context, token return nil } -func (cp *credentialsProvider) authorizeIfTokenExists(ctx context.Context) error { +func (cp *credentialsProvider) authorizeIfTokenExists(ctx context.Context) { token, err := cp.tokenStore.Get(ctx, cp.id) if err == nil && token.RefreshToken != "" { cp.logger.Info("token exists") @@ -437,8 +435,6 @@ func (cp *credentialsProvider) authorizeIfTokenExists(ctx context.Context) error } else { cp.setUnauthorizedStatus(errors.WithStack(ErrAuthorizationNeeded)) } - - return nil } // validateConfig validates the configuration and returns an error if any required field is missing.