diff --git a/util/oauth/helper.go b/util/oauth/helper.go new file mode 100644 index 0000000000..94f322be27 --- /dev/null +++ b/util/oauth/helper.go @@ -0,0 +1,38 @@ +package oauth + +import ( + "time" + + "github.com/evcc-io/evcc/util" + "golang.org/x/oauth2" +) + +// Refresh refreshes the token every 5m. If token refresh fails 5 times, it is aborted. +func Refresh(log *util.Logger, token *oauth2.Token, ts oauth2.TokenSource, optMaxTokenLifetime ...time.Duration) { + var failed int + + for range time.Tick(5 * time.Minute) { + if _, err := ts.Token(); err != nil { + t, err := ts.Token() + if err != nil { + failed++ + if failed > 5 { + log.ERROR.Printf("token refresh: %v, giving up", err) + return + } + log.ERROR.Printf("token refresh: %v", err) + } + + failed = 0 + + // limit lifetime of new tokens + if len(optMaxTokenLifetime) == 1 && t.Expiry != token.Expiry { + token = t + maxTokenLifetime := optMaxTokenLifetime[0] + if time.Until(token.Expiry) > maxTokenLifetime { + token.Expiry = time.Now().Add(maxTokenLifetime) + } + } + } + } +} diff --git a/vehicle/porsche/identity.go b/vehicle/porsche/identity.go index 9f37a3830f..8ac5343134 100644 --- a/vehicle/porsche/identity.go +++ b/vehicle/porsche/identity.go @@ -8,11 +8,11 @@ import ( "net/http" "net/http/cookiejar" "net/url" - "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" cv "github.com/nirasan/go-oauth-pkce-code-verifier" "github.com/samber/lo" @@ -22,6 +22,8 @@ import ( const ( OAuthURI = "https://identity.porsche.com" ClientID = "UYsK00My6bCqJdbQhTQ0PbWmcSdIAMig" + + maxTokenLifetime = time.Hour ) // https://identity.porsche.com/.well-known/openid-configuration @@ -157,35 +159,8 @@ func (v *Identity) Login(oc *oauth2.Config, user, password string) (oauth2.Token return nil, err } - if maxDuration := time.Hour; time.Until(token.Expiry) > maxDuration { - token.Expiry = time.Now().Add(maxDuration) - } - ts := oauth2.ReuseTokenSourceWithExpiry(token, oc.TokenSource(cctx, token), 15*time.Minute) - go v.refresh(token, ts) + go oauth.Refresh(v.log, token, ts, maxTokenLifetime) return ts, err } - -func (v *Identity) refresh(initial *oauth2.Token, ts oauth2.TokenSource) { - token := initial - - for range time.Tick(5 * time.Minute) { - t, err := ts.Token() - if err != nil { - v.log.ERROR.Printf("token refresh: %v", err) - if strings.Contains(err.Error(), "invalid_grant") { - return - } - } - - // limit lifetime of new tokens - if t.Expiry != token.Expiry { - token = t - if maxDuration := time.Hour; time.Until(token.Expiry) > maxDuration { - token.Expiry = time.Now().Add(maxDuration) - v.log.TRACE.Printf("token refresh: lifetime limited to %v", maxDuration) - } - } - } -} diff --git a/vehicle/volvo-connected.go b/vehicle/volvo-connected.go index c3f7bce9b3..741e84735c 100644 --- a/vehicle/volvo-connected.go +++ b/vehicle/volvo-connected.go @@ -64,20 +64,19 @@ func NewVolvoConnectedFromConfig(other map[string]interface{}) (api.Vehicle, err return nil, err } - if err := identity.Login(cc.User, cc.Password); err != nil { + ts, err := identity.Login(cc.User, cc.Password) + if err != nil { return nil, err } - _ = identity // api := connected.NewAPI(log, identity, cc.Sandbox) - api := connected.NewAPI(log, identity, cc.VccApiKey) + api := connected.NewAPI(log, ts, cc.VccApiKey) cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) v := &VolvoConnected{ embed: &cc.embed, Provider: connected.NewProvider(api, cc.VIN, cc.Cache), - // ProviderLogin: identity, // expose the OAuth2 login } return v, err diff --git a/vehicle/volvo/connected/identity.go b/vehicle/volvo/connected/identity.go index 235e939a28..2a56cb4a2b 100644 --- a/vehicle/volvo/connected/identity.go +++ b/vehicle/volvo/connected/identity.go @@ -4,6 +4,7 @@ import ( "net/http" "net/url" "strings" + "time" "github.com/coreos/go-oidc/v3/oidc" "github.com/evcc-io/evcc/util" @@ -17,7 +18,8 @@ var Oauth2Config = oauth2.Config{ AuthURL: "https://volvoid.eu.volvocars.com/as/authorization.oauth2", TokenURL: "https://volvoid.eu.volvocars.com/as/token.oauth2", }, - Scopes: []string{oidc.ScopeOpenID, "vehicle:attributes", + Scopes: []string{ + oidc.ScopeOpenID, "vehicle:attributes", "energy:recharge_status", "energy:battery_charge_level", "energy:electric_range", "energy:estimated_charging_time", "energy:charging_connection_status", "energy:charging_system_status", "conve:fuel_status", "conve:odometer_status", "conve:environment", }, @@ -29,19 +31,20 @@ const ( ) type Identity struct { + log *util.Logger *request.Helper - oauth2.TokenSource } func NewIdentity(log *util.Logger) (*Identity, error) { v := &Identity{ + log: log, Helper: request.NewHelper(log), } return v, nil } -func (v *Identity) Login(user, password string) error { +func (v *Identity) Login(user, password string) (oauth2.TokenSource, error) { data := url.Values{ "username": {user}, "password": {password}, @@ -54,15 +57,20 @@ func (v *Identity) Login(user, password string) error { "Content-Type": request.FormContent, "Authorization": basicAuth, }) + if err != nil { + return nil, err + } - if err == nil { - var token oauth2.Token - if err = v.DoJSON(req, &token); err == nil { - v.TokenSource = oauth.RefreshTokenSource(&token, v) - } + var token oauth.Token + if err := v.DoJSON(req, &token); err != nil { + return nil, err } - return err + oauthToken := (*oauth2.Token)(&token) + ts := oauth2.ReuseTokenSourceWithExpiry(oauthToken, oauth.RefreshTokenSource(oauthToken, v), 15*time.Minute) + go oauth.Refresh(v.log, oauthToken, ts) + + return ts, nil } func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { @@ -76,11 +84,12 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { "Content-Type": request.FormContent, "Authorization": basicAuth, }) + if err != nil { + return nil, err + } var res oauth2.Token - if err == nil { - err = v.DoJSON(req, &res) - } + err = v.DoJSON(req, &res) return &res, err }