From 2691bbda1b0150f11c960f72a993a1f78e553b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Sun, 25 Jan 2026 14:15:54 +0100 Subject: [PATCH] chore: reuse user token source (#26144) --- charger/easee/identity.go | 25 +++++++++----- charger/zaptec.go | 2 +- charger/zaptec/auth.go | 73 ++++++++++++++++----------------------- util/cache/cache.go | 37 ++++++++++++++++++++ 4 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 util/cache/cache.go diff --git a/charger/easee/identity.go b/charger/easee/identity.go index 164d64b40..e4dedf409 100644 --- a/charger/easee/identity.go +++ b/charger/easee/identity.go @@ -6,6 +6,7 @@ import ( "time" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/cache" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" "golang.org/x/oauth2" @@ -38,17 +39,25 @@ type tokenSource struct { user, password string } -// TokenSource creates an Easee token source +// tokenSourceCache stores per-user token sources +var tokenSourceCache = cache.New[oauth2.TokenSource]() + +// TokenSource returns a shared oauth2.TokenSource for the given user. func TokenSource(log *util.Logger, user, password string) (oauth2.TokenSource, error) { - c := &tokenSource{ - Helper: request.NewHelper(log), - user: user, - password: password, - } + return tokenSourceCache.GetOrCreate(user, func() (oauth2.TokenSource, error) { + c := &tokenSource{ + Helper: request.NewHelper(log), + user: user, + password: password, + } - token, err := c.authenticate() + token, err := c.authenticate() + if err != nil { + return nil, err + } - return oauth.RefreshTokenSource(token.AsOAuth2Token(), c.refreshToken), err + return oauth.RefreshTokenSource(token.AsOAuth2Token(), c.refreshToken), nil + }) } func (c *tokenSource) authenticate() (*Token, error) { diff --git a/charger/zaptec.go b/charger/zaptec.go index 5f5002992..70757197a 100644 --- a/charger/zaptec.go +++ b/charger/zaptec.go @@ -121,7 +121,7 @@ func NewZaptec(ctx context.Context, user, password, id string, priority bool, pa }) // Get shared token source for this user (per-user uniqueness) - ts, err := zaptec.GetTokenSource(tsCtx, user, password) + ts, err := zaptec.TokenSource(tsCtx, user, password) if err != nil { return nil, err } diff --git a/charger/zaptec/auth.go b/charger/zaptec/auth.go index 201fb0f69..e1c509917 100644 --- a/charger/zaptec/auth.go +++ b/charger/zaptec/auth.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/coreos/go-oidc/v3/oidc" + "github.com/evcc-io/evcc/util/cache" "golang.org/x/oauth2" ) @@ -23,10 +24,9 @@ func (p *passwordTokenSource) Token() (*oauth2.Token, error) { return p.config.PasswordCredentialsToken(p.ctx, p.user, p.pass) } -// tokenSourceCache stores per-user token sources var ( - tokenSourceMu sync.Mutex - tokenSourceCache = make(map[string]oauth2.TokenSource) + // TokenSourceCache stores per-user token sources + tokenSourceCache = cache.New[oauth2.TokenSource]() oidcProvider *oidc.Provider oidcProviderOnce sync.Once @@ -41,48 +41,35 @@ func getOIDCProvider(ctx context.Context) (*oidc.Provider, error) { return oidcProvider, oidcProviderErr } -// GetTokenSource returns a shared oauth2.TokenSource for the given user credentials. -// Multiple chargers using the same user credentials will share the same TokenSource, -// ensuring tokens are reused and authentication is deduplicated. -func GetTokenSource(ctx context.Context, user, pass string) (oauth2.TokenSource, error) { - tokenSourceMu.Lock() - defer tokenSourceMu.Unlock() +// TokenSource returns a shared oauth2.TokenSource for the given user. +func TokenSource(ctx context.Context, user, pass string) (oauth2.TokenSource, error) { + return tokenSourceCache.GetOrCreate(user, func() (oauth2.TokenSource, error) { + provider, err := getOIDCProvider(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err) + } - // Use username as the cache key (assuming username is unique) - if ts, exists := tokenSourceCache[user]; exists { - return ts, nil - } + oc := &oauth2.Config{ + Endpoint: provider.Endpoint(), + Scopes: []string{ + oidc.ScopeOpenID, + }, + } - // Get the cached OIDC provider (initialized once) - provider, err := getOIDCProvider(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err) - } + // Create the password token source + pts := &passwordTokenSource{ + ctx: ctx, + config: oc, + user: user, + pass: pass, + } - oc := &oauth2.Config{ - Endpoint: provider.Endpoint(), - Scopes: []string{ - oidc.ScopeOpenID, - }, - } + // Get initial token + token, err := pts.Token() + if err != nil { + return nil, err + } - // Create the password token source - pts := &passwordTokenSource{ - ctx: ctx, - config: oc, - user: user, - pass: pass, - } - - // Get initial token - token, err := pts.Token() - if err != nil { - return nil, err - } - - // Wrap with ReuseTokenSource to cache tokens - ts := oauth2.ReuseTokenSource(token, pts) - tokenSourceCache[user] = ts - - return ts, nil + return oauth2.ReuseTokenSource(token, pts), nil + }) } diff --git a/util/cache/cache.go b/util/cache/cache.go new file mode 100644 index 000000000..7c7b6024f --- /dev/null +++ b/util/cache/cache.go @@ -0,0 +1,37 @@ +package cache + +import ( + "sync" +) + +// Cache provides thread-safe caching keyed by username +type Cache[T any] struct { + mu sync.Mutex + cache map[string]T +} + +// New creates a new Cache instance +func New[T any]() *Cache[T] { + return &Cache[T]{ + cache: make(map[string]T), + } +} + +// GetOrCreate atomically gets or creates a cached object +func (c *Cache[T]) GetOrCreate(key string, createFn func() (T, error)) (T, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if res, ok := c.cache[key]; ok { + return res, nil + } + + res, err := createFn() + if err != nil { + var zero T + return zero, err + } + + c.cache[key] = res + return res, nil +}