Volvo Connected: fix token refresh (#8998)
This commit is contained in:
parent
f864ddb72f
commit
66b32d83f8
4 changed files with 67 additions and 46 deletions
38
util/oauth/helper.go
Normal file
38
util/oauth/helper.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue