Skip to content

Commit

Permalink
Volvo Connected: fix token refresh (evcc-io#8998)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jul 17, 2023
1 parent f864ddb commit 66b32d8
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 45 deletions.
38 changes: 38 additions & 0 deletions util/oauth/helper.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
33 changes: 4 additions & 29 deletions vehicle/porsche/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -22,6 +22,8 @@ import (
const (
OAuthURI = "https://identity.porsche.com"
ClientID = "UYsK00My6bCqJdbQhTQ0PbWmcSdIAMig"

maxTokenLifetime = time.Hour
)

// https://identity.porsche.com/.well-known/openid-configuration
Expand Down Expand Up @@ -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)
}
}
}
}
7 changes: 3 additions & 4 deletions vehicle/volvo-connected.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 21 additions & 12 deletions vehicle/volvo/connected/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"net/url"
"strings"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/evcc-io/evcc/util"
Expand All @@ -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",
},
Expand All @@ -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},
Expand All @@ -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) {
Expand All @@ -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
}

0 comments on commit 66b32d8

Please sign in to comment.