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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,44 +19,46 @@ import (
|
|||
// by request state obtained from the request and delegates to the registered handler.
|
||||
type Handler struct {
|
||||
mu sync.Mutex
|
||||
log *util.Logger
|
||||
secret []byte
|
||||
providers map[string]api.AuthProvider
|
||||
states map[string]string
|
||||
log *util.Logger
|
||||
updateC chan string
|
||||
}
|
||||
|
||||
func (a *Handler) Publish(paramC chan<- util.Param) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
// TODO get status from update channel
|
||||
func (a *Handler) run(paramC chan<- util.Param) {
|
||||
for range <-a.updateC {
|
||||
a.mu.Lock()
|
||||
|
||||
apMap := make(map[string]*AuthProvider)
|
||||
|
||||
for id, provider := range a.providers {
|
||||
ap := &AuthProvider{
|
||||
ID: url.QueryEscape(id),
|
||||
Authenticated: provider.Authenticated(),
|
||||
res := make(map[string]*AuthProvider)
|
||||
for id, provider := range a.providers {
|
||||
res[provider.DisplayName()] = &AuthProvider{
|
||||
ID: url.QueryEscape(id),
|
||||
Authenticated: provider.Authenticated(),
|
||||
}
|
||||
}
|
||||
apMap[provider.DisplayName()] = ap
|
||||
|
||||
a.mu.Unlock()
|
||||
|
||||
a.log.TRACE.Printf("publishing %d auth providers", len(res))
|
||||
|
||||
// publish the updated auth providers
|
||||
paramC <- util.Param{Key: keys.AuthProviders, Val: res}
|
||||
}
|
||||
|
||||
a.log.TRACE.Printf("publishing %d auth providers", len(apMap))
|
||||
|
||||
// publish the updated auth providers
|
||||
paramC <- util.Param{Key: keys.AuthProviders, Val: apMap}
|
||||
}
|
||||
|
||||
func (a *Handler) register(name string, handler api.AuthProvider) error {
|
||||
func (a *Handler) register(name string, handler api.AuthProvider) (chan<- string, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.providers[name] != nil {
|
||||
return fmt.Errorf("provider already registered: %s", name)
|
||||
return nil, fmt.Errorf("provider already registered: %s", name)
|
||||
}
|
||||
|
||||
a.log.DEBUG.Printf("registering provider: %s", name)
|
||||
a.providers[name] = handler
|
||||
|
||||
return nil
|
||||
return a.updateC, nil
|
||||
}
|
||||
|
||||
func (a *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import (
|
|||
"crypto/rand"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
|
|
@ -26,11 +24,11 @@ func init() {
|
|||
}
|
||||
|
||||
instance = &Handler{
|
||||
mu: sync.Mutex{},
|
||||
log: util.NewLogger("providerauth"),
|
||||
secret: secret[:],
|
||||
providers: make(map[string]api.AuthProvider),
|
||||
states: make(map[string]string),
|
||||
log: util.NewLogger("providerauth"),
|
||||
updateC: make(chan string, 1),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,16 +41,24 @@ func Setup(router *mux.Router, paramC chan<- util.Param) {
|
|||
// logout?id=...
|
||||
router.Methods(http.MethodGet).Path("/logout").HandlerFunc(instance.handleLogout)
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
go instance.run(paramC)
|
||||
}
|
||||
|
||||
// Register registers a specific AuthProvider by name
|
||||
// The returned online channel is used to asynchronously update authorization status
|
||||
func Register(name string, handler api.AuthProvider) (chan<- bool, error) {
|
||||
updateC, err := instance.register(name, handler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
onlineC := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
instance.Publish(paramC)
|
||||
for range onlineC {
|
||||
updateC <- name
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Register registers a specific AuthProvider. Returns login path as string.
|
||||
func Register(name string, handler api.AuthProvider) error {
|
||||
return instance.register(name, handler)
|
||||
return onlineC, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ template: vaillant
|
|||
products:
|
||||
- brand: Vaillant
|
||||
description:
|
||||
generic: SensoNET (SG Ready)
|
||||
generic: SensoNET (API)
|
||||
group: heating
|
||||
requirements:
|
||||
# evcc: ["sponsorship"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ template: viessmann
|
|||
products:
|
||||
- brand: Viessmann
|
||||
description:
|
||||
generic: Heatpump (SG Ready)
|
||||
generic: Heatpump (API)
|
||||
group: heating
|
||||
requirements:
|
||||
# evcc: ["sponsorship"]
|
||||
|
|
@ -14,20 +14,11 @@ requirements:
|
|||
One-time hot water preparation. The device automatically decides whether to use the heat pump or the auxiliary electric heater (if available).
|
||||
params:
|
||||
- name: user
|
||||
required: true
|
||||
help:
|
||||
de: Registrieren auf [app.developer.viessmann-climatesolutions.com](https://app.developer.viessmann-climatesolutions.com)
|
||||
en: Register at [app.developer.viessmann-climatesolutions.com](https://app.developer.viessmann-climatesolutions.com)
|
||||
deprecated: true
|
||||
- name: password
|
||||
required: true
|
||||
help:
|
||||
de: Für den konfigurierten Viessmann Account.
|
||||
en: For the configured Viessmann account.
|
||||
deprecated: true
|
||||
- name: clientid
|
||||
required: true
|
||||
description:
|
||||
de: Client ID
|
||||
en: Client ID
|
||||
help:
|
||||
de: Konfigurieren in [app.developer.viessmann-climatesolutions.com](https://app.developer.viessmann-climatesolutions.com)
|
||||
en: Configure at [app.developer.viessmann-climatesolutions.com](https://app.developer.viessmann-climatesolutions.com)
|
||||
|
|
@ -158,8 +149,6 @@ render: |
|
|||
cache: 2s # to prevent making two identical requests straight after each other for "getmode"
|
||||
auth:
|
||||
source: viessmann
|
||||
user: {{ .user }}
|
||||
password: {{ .password }}
|
||||
clientid: {{ .clientid }}
|
||||
jq: '.data.properties.active.value | if . == false then 2 elif . == true then 3 else . end'
|
||||
# false -> oneTimeCharge is disabled -> normal mode -> 2
|
||||
|
|
@ -180,8 +169,6 @@ render: |
|
|||
- content-type: application/json
|
||||
auth:
|
||||
source: viessmann
|
||||
user: {{ .user }}
|
||||
password: {{ .password }}
|
||||
clientid: {{ .clientid }}
|
||||
body: >
|
||||
{ }
|
||||
|
|
@ -194,8 +181,6 @@ render: |
|
|||
- content-type: application/json
|
||||
auth:
|
||||
source: viessmann
|
||||
user: {{ .user }}
|
||||
password: {{ .password }}
|
||||
clientid: {{ .clientid }}
|
||||
body: >
|
||||
{ }
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import (
|
|||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/plugin/auth"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/volvo/connected"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// VolvoConnected is an api.Vehicle implementation for Volvo Connected Car vehicles
|
||||
|
|
@ -41,10 +39,7 @@ func NewVolvoConnectedFromConfig(ctx context.Context, other map[string]interface
|
|||
|
||||
log := util.NewLogger("volvo-connected").Redact(cc.VIN, cc.VccApiKey)
|
||||
|
||||
// create oauth2 config
|
||||
oc := connected.Oauth2Config(cc.Credentials.ID, cc.Credentials.Secret, cc.RedirectUri)
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log))
|
||||
|
||||
ts, err := auth.NewOauth(ctx, cc.embed.GetTitle(), oc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue