chore: reuse user token source (#26144)

This commit is contained in:
Michael Heß 2026-01-25 14:15:54 +01:00 • committed by GitHub
parent 2b13eb2668
commit 2691bbda1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 85 additions and 52 deletions

View file

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/evcc-io/evcc/util" "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/oauth"
"github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -38,17 +39,25 @@ type tokenSource struct {
user, password string 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) { func TokenSource(log *util.Logger, user, password string) (oauth2.TokenSource, error) {
c := &tokenSource{ return tokenSourceCache.GetOrCreate(user, func() (oauth2.TokenSource, error) {
Helper: request.NewHelper(log), c := &tokenSource{
user: user, Helper: request.NewHelper(log),
password: password, 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) { func (c *tokenSource) authenticate() (*Token, error) {

View file

@ -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) // 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -6,6 +6,7 @@ import (
"sync" "sync"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/evcc-io/evcc/util/cache"
"golang.org/x/oauth2" "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) return p.config.PasswordCredentialsToken(p.ctx, p.user, p.pass)
} }
// tokenSourceCache stores per-user token sources
var ( var (
tokenSourceMu sync.Mutex // TokenSourceCache stores per-user token sources
tokenSourceCache = make(map[string]oauth2.TokenSource) tokenSourceCache = cache.New[oauth2.TokenSource]()
oidcProvider *oidc.Provider oidcProvider *oidc.Provider
oidcProviderOnce sync.Once oidcProviderOnce sync.Once
@ -41,48 +41,35 @@ func getOIDCProvider(ctx context.Context) (*oidc.Provider, error) {
return oidcProvider, oidcProviderErr return oidcProvider, oidcProviderErr
} }
// GetTokenSource returns a shared oauth2.TokenSource for the given user credentials. // TokenSource returns a shared oauth2.TokenSource for the given user.
// Multiple chargers using the same user credentials will share the same TokenSource, func TokenSource(ctx context.Context, user, pass string) (oauth2.TokenSource, error) {
// ensuring tokens are reused and authentication is deduplicated. return tokenSourceCache.GetOrCreate(user, func() (oauth2.TokenSource, error) {
func GetTokenSource(ctx context.Context, user, pass string) (oauth2.TokenSource, error) { provider, err := getOIDCProvider(ctx)
tokenSourceMu.Lock() if err != nil {
defer tokenSourceMu.Unlock() return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
}
// Use username as the cache key (assuming username is unique) oc := &oauth2.Config{
if ts, exists := tokenSourceCache[user]; exists { Endpoint: provider.Endpoint(),
return ts, nil Scopes: []string{
} oidc.ScopeOpenID,
},
}
// Get the cached OIDC provider (initialized once) // Create the password token source
provider, err := getOIDCProvider(ctx) pts := &passwordTokenSource{
if err != nil { ctx: ctx,
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err) config: oc,
} user: user,
pass: pass,
}
oc := &oauth2.Config{ // Get initial token
Endpoint: provider.Endpoint(), token, err := pts.Token()
Scopes: []string{ if err != nil {
oidc.ScopeOpenID, return nil, err
}, }
}
// Create the password token source return oauth2.ReuseTokenSource(token, pts), nil
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
} }

37
util/cache/cache.go vendored Normal file
View file

@ -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
}