Provider authorization: handle updates async (#24097)
This commit is contained in:
parent
1e79b43ab6
commit
9f26560075
7 changed files with 88 additions and 132 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue