Zaptec: add per-user token caching (#26109)
This commit is contained in:
parent
26d61914f9
commit
86aeb8e542
2 changed files with 101 additions and 42 deletions
|
|
@ -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
88
charger/zaptec/auth.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue