Provider authorization: handle updates async (#24097)

This commit is contained in:
andig 2025-10-04 15:51:27 +02:00 • committed by GitHub
parent 1e79b43ab6
commit 9f26560075
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 88 additions and 132 deletions

View file

@ -16,6 +16,7 @@ import (
"github.com/evcc-io/evcc/server/providerauth"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)
@ -27,6 +28,7 @@ type OAuth struct {
subject string
cv string
ctx context.Context
onlineC chan<- bool
deviceFlow bool
tokenRetriever func(string, *oauth2.Token) error
@ -107,11 +109,16 @@ func NewOauth(ctx context.Context, name string, oc *oauth2.Config, opts ...oauth
return instance, nil
}
// create new instance
log := util.NewLogger("oauth-" + hash)
if ctx.Value(oauth2.HTTPClient) == nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log))
}
o := &OAuth{
subject: subject,
oc: oc,
log: util.NewLogger("oauth"),
log: log,
ctx: ctx,
}
@ -142,12 +149,18 @@ func NewOauth(ctx context.Context, name string, oc *oauth2.Config, opts ...oauth
o.TokenSource = oauth.RefreshTokenSource(&token, o)
// register auth redirect
onlineC, err := providerauth.Register(subject, o)
if err != nil {
return nil, err
}
o.onlineC = onlineC
o.onlineC <- token.Valid()
// add instance
addInstance(o.subject, o)
// register auth redirect
providerauth.Register(subject, o)
return o, nil
}
@ -163,19 +176,20 @@ func (o *OAuth) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
token, err := o.oc.TokenSource(o.ctx, token).Token()
if err != nil {
if strings.Contains(err.Error(), "invalid_grant") && settings.Exists(o.subject) {
o.onlineC <- false
settings.Delete(o.subject)
}
return nil, err
}
err = settings.SetJson(o.subject, token)
err = o.updateToken(token)
return token, err
}
// updateToken must only be called when lock is held
func (o *OAuth) updateToken(token *oauth2.Token) {
func (o *OAuth) updateToken(token *oauth2.Token) error {
var store any = token
// tokenStorer allows persisting the token together with it's extra properties
@ -184,7 +198,19 @@ func (o *OAuth) updateToken(token *oauth2.Token) {
}
if err := settings.SetJson(o.subject, store); err != nil {
return err
}
o.onlineC <- token.Valid()
return nil
}
// updateTokenSource must only be called when lock is held
func (o *OAuth) updateTokenSource(token *oauth2.Token) {
if err := o.updateToken(token); err != nil {
o.log.ERROR.Printf("error saving token: %v", err)
return
}
o.TokenSource = oauth.RefreshTokenSource(token, o)
@ -202,7 +228,7 @@ func (o *OAuth) HandleCallback(params url.Values) error {
return err
}
o.updateToken(token)
o.updateTokenSource(token)
return nil
}
@ -215,15 +241,13 @@ func (o *OAuth) Login(state string) (string, error) {
o.cv = oauth2.GenerateVerifier()
if o.deviceFlow {
ctx := context.Background()
da, err := o.oc.DeviceAuth(ctx, oauth2.S256ChallengeOption(o.cv))
da, err := o.oc.DeviceAuth(o.ctx, oauth2.S256ChallengeOption(o.cv))
if err != nil {
return "", err
}
go func() {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
ctx, cancel := context.WithTimeout(o.ctx, 5*time.Minute)
defer cancel()
token, err := o.oc.DeviceAccessToken(ctx, da, oauth2.VerifierOption(o.cv))
@ -235,7 +259,7 @@ func (o *OAuth) Login(state string) (string, error) {
o.mu.Lock()
defer o.mu.Unlock()
o.updateToken(token)
o.updateTokenSource(token)
}()
return da.VerificationURIComplete, nil
@ -261,7 +285,9 @@ func (o *OAuth) Logout() error {
o.mu.Lock()
defer o.mu.Unlock()
o.onlineC <- false
o.TokenSource = oauth.RefreshTokenSource(nil, o)
return nil
}

View file

@ -2,15 +2,9 @@ package auth
import (
"context"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
@ -20,7 +14,7 @@ const (
// ^ the value of RedirectURI doesn't matter, but it must be the same between requests
)
func OAuth2Config(clientID string) *oauth2.Config {
func oauth2Config(clientID string) *oauth2.Config {
return &oauth2.Config{
ClientID: clientID,
Endpoint: oauth2.Endpoint{
@ -37,69 +31,17 @@ func init() {
registry.AddCtx("viessmann", NewViessmannFromConfig)
}
type Viessmann struct {
client *http.Client
}
func NewViessmannFromConfig(ctx context.Context, other map[string]any) (oauth2.TokenSource, error) {
var cc struct {
ClientID string
User, Password string
ClientID string
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
v := &Viessmann{
client: request.NewClient(util.NewLogger("viessmann")),
}
log := util.NewLogger("viessmann").Redact(cc.ClientID)
ctx = context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log))
oc := OAuth2Config(cc.ClientID)
ctx = context.WithValue(ctx, oauth2.HTTPClient, v.client)
token, err := v.login(ctx, oc, cc.User, cc.Password)
if err != nil {
return nil, err
}
return oc.TokenSource(ctx, token), nil
}
func (v *Viessmann) login(ctx context.Context, oc *oauth2.Config, user, password string) (*oauth2.Token, error) {
cv := oauth2.GenerateVerifier()
state := lo.RandomString(16, lo.AlphanumericCharset)
uri := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(cv))
v.client.Jar, _ = cookiejar.New(nil)
v.client.CheckRedirect = request.DontFollow
defer func() {
v.client.Jar = nil
v.client.CheckRedirect = nil
}()
req, _ := request.New(http.MethodGet, uri, nil, map[string]string{
"Authorization": transport.BasicAuthHeader(user, password),
})
resp, err := v.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
return nil, fmt.Errorf("unexpected status %d", resp.StatusCode)
}
loc, err := url.Parse(resp.Header.Get("Location"))
if err != nil {
return nil, err
}
code := loc.Query().Get("code")
ctx, cancel := context.WithTimeout(ctx, request.Timeout)
defer cancel()
return oc.Exchange(ctx, code, oauth2.VerifierOption(cv))
return NewOauth(ctx, "Viessmann", oauth2Config(cc.ClientID))
}