Volvo Connected: fix token refresh (#8998)

This commit is contained in:
andig 2023-07-17 20:36:36 +02:00 • committed by GitHub
parent f864ddb72f
commit 66b32d83f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 46 deletions

38
util/oauth/helper.go Normal file
View file

@ -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)
}
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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

View file

@ -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 {
var token oauth2.Token
if err = v.DoJSON(req, &token); err == nil {
v.TokenSource = oauth.RefreshTokenSource(&token, v)
}
if err != nil {
return nil, err
}
return err
var token oauth.Token
if err := v.DoJSON(req, &token); err != nil {
return nil, 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
}