Zaptec: add per-user token caching (#26109)

This commit is contained in:
andig 2025-12-20 15:10:34 +01:00 • committed by GitHub
parent 26d61914f9
commit 86aeb8e542
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 42 deletions

View file

@ -26,12 +26,12 @@ import (
"sort"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/zaptec"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/evcc-io/evcc/util/transport"
"golang.org/x/oauth2"
)
@ -55,18 +55,6 @@ func init() {
registry.AddCtx("zaptec", NewZaptecFromConfig)
}
// passwordTokenSource implements oauth2.TokenSource for password grant flow
type passwordTokenSource struct {
ctx context.Context
config *oauth2.Config
user string
pass string
}
func (p passwordTokenSource) Token() (*oauth2.Token, error) {
return p.config.PasswordCredentialsToken(p.ctx, p.user, p.pass)
}
//go:generate go tool decorate -f decorateZaptec -b *Zaptec -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error"
// NewZaptecFromConfig creates a Zaptec Pro charger from generic config
@ -107,6 +95,14 @@ func NewZaptec(ctx context.Context, user, password, id string, priority bool, pa
passive: passive,
}
// Add User-Agent header for Zaptec API compliance
c.Client.Transport = &transport.Decorator{
Decorator: transport.DecorateHeaders(map[string]string{
"User-Agent": "evcc/" + util.Version,
}),
Base: c.Client.Transport,
}
// setup cached values
c.statusG = util.ResettableCached(func() (zaptec.StateResponse, error) {
var res zaptec.StateResponse
@ -117,43 +113,18 @@ func NewZaptec(ctx context.Context, user, password, id string, priority bool, pa
return res, err
}, cache)
provider, err := oidc.NewProvider(ctx, zaptec.ApiURL+"/")
if err != nil {
return nil, fmt.Errorf("failed to initialize OIDC provider: %s", err)
}
oc := &oauth2.Config{
Endpoint: provider.Endpoint(),
Scopes: []string{
oidc.ScopeOpenID,
},
}
// Create a separate HTTP client for OAuth token requests to avoid circular dependency
// (c.Transport will be modified to use oauth2.Transport, which would create a loop)
tokenClient := &http.Client{
tsCtx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{
Transport: c.Transport,
}
})
oauthCtx := context.WithValue(
ctx,
oauth2.HTTPClient,
tokenClient,
)
token, err := oc.PasswordCredentialsToken(oauthCtx, user, password)
// Get shared token source for this user (per-user uniqueness)
ts, err := zaptec.GetTokenSource(tsCtx, user, password)
if err != nil {
return nil, err
}
// Create custom token source that always uses password grant (no refresh tokens)
ts := oauth2.ReuseTokenSource(token, passwordTokenSource{
ctx: oauthCtx,
config: oc,
user: user,
pass: password,
})
c.Transport = &oauth2.Transport{
Source: ts,
Base: c.Transport,

88
charger/zaptec/auth.go Normal file
View file

@ -0,0 +1,88 @@
package zaptec
import (
"context"
"fmt"
"sync"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
// passwordTokenSource implements oauth2.TokenSource for password grant flow
type passwordTokenSource struct {
ctx context.Context
config *oauth2.Config
user string
pass string
}
// Token returns a token or an error.
// Implements oauth2.TokenSource interface
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)
oidcProvider *oidc.Provider
oidcProviderOnce sync.Once
oidcProviderErr error
)
// getOIDCProvider returns the cached OIDC provider, initializing it once if needed
func getOIDCProvider(ctx context.Context) (*oidc.Provider, error) {
oidcProviderOnce.Do(func() {
oidcProvider, oidcProviderErr = oidc.NewProvider(ctx, ApiURL+"/")
})
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()
// Use username as the cache key (assuming username is unique)
if ts, exists := tokenSourceCache[user]; exists {
return ts, nil
}
// 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)
}
oc := &oauth2.Config{
Endpoint: provider.Endpoint(),
Scopes: []string{
oidc.ScopeOpenID,
},
}
// 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
}